diff --git a/internal/adapters/primary/cli/new.go b/internal/adapters/primary/cli/new.go index 537212b..3dd96ed 100644 --- a/internal/adapters/primary/cli/new.go +++ b/internal/adapters/primary/cli/new.go @@ -22,8 +22,8 @@ const ( ) var ( - projectType string - quietNew bool + projectType string //nolint:gochecknoglobals // cobra flag variable + quietNew bool //nolint:gochecknoglobals // cobra flag variable ) const dprTemplate = `program %s; @@ -120,7 +120,8 @@ const dprojTemplate = ` - + ` @@ -138,16 +139,16 @@ func generateGUID() string { // newCmdRegister registers the new command. func newCmdRegister(root *cobra.Command) { var newCmd = &cobra.Command{ - Use: "new [project_name]", - Short: "Create a new Delphi project skeleton", - Long: "Create a new Delphi project skeleton with source directories, templates, and boss.json", - Args: cobra.MaximumNArgs(1), + Use: "new [project_name]", + Short: "Create a new Delphi project skeleton", + Long: "Create a new Delphi project skeleton with source directories, templates, and boss.json", + Args: cobra.MaximumNArgs(1), Example: ` Create a new console application: boss new my_project Create a new package/library: boss new my_package --type pkg`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { var name string if len(args) > 0 { name = args[0] @@ -194,9 +195,11 @@ func doCreateProject(name string, pType string, quiet bool) { // Create directories srcDir := filepath.Join(projectDir, "src") testsDir := filepath.Join(projectDir, "tests") + //nolint:gosec // Standard directory permissions if err := os.MkdirAll(srcDir, 0755); err != nil { msg.Die("❌ Failed to create src directory: %v", err) } + //nolint:gosec // Standard directory permissions if err := os.MkdirAll(testsDir, 0755); err != nil { msg.Die("❌ Failed to create tests directory: %v", err) } @@ -207,8 +210,8 @@ func doCreateProject(name string, pType string, quiet bool) { packageData.Version = defaultPackageVersion packageData.MainSrc = "src" - packageJsonPath := filepath.Join(projectDir, consts.FilePackage) - if err := pkgmanager.SavePackage(packageData, packageJsonPath); err != nil { + packageJSONPath := filepath.Join(projectDir, consts.FilePackage) + if err := pkgmanager.SavePackage(packageData, packageJSONPath); err != nil { msg.Die("❌ Failed to save boss.json: %v", err) } diff --git a/internal/adapters/primary/cli/new_test.go b/internal/adapters/primary/cli/new_test.go index c116f57..7800002 100644 --- a/internal/adapters/primary/cli/new_test.go +++ b/internal/adapters/primary/cli/new_test.go @@ -39,9 +39,11 @@ func TestNewCommandRegistration(t *testing.T) { } typeFlag := newCmd.Flags().Lookup("type") + //nolint:staticcheck // Test intentionally keeps non-fatal assertion before the follow-up check if typeFlag == nil { t.Error("New command should have --type flag") } + //nolint:staticcheck // Test handles nil case with t.Error if typeFlag.DefValue != "app" { t.Errorf("New command --type flag default value should be 'app', got %s", typeFlag.DefValue) } @@ -55,15 +57,9 @@ func TestNewCommandRegistration(t *testing.T) { // TestDoCreateProject_App tests bootstrapping an application project. func TestDoCreateProject_App(t *testing.T) { tempDir := t.TempDir() + t.Chdir(tempDir) - // Redirect working directory - oldWd, err := os.Getwd() - if err == nil { - defer func() { _ = os.Chdir(oldWd) }() - } - if err := os.Chdir(tempDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } + var err error // Initialize package manager fs := filesystem.NewOSFileSystem() @@ -76,31 +72,31 @@ func TestDoCreateProject_App(t *testing.T) { doCreateProject(projectName, "app", true) projectPath := filepath.Join(tempDir, projectName) - if _, err := os.Stat(projectPath); os.IsNotExist(err) { + if _, statErr := os.Stat(projectPath); os.IsNotExist(statErr) { t.Fatalf("Project directory was not created") } // Check folders - if _, err := os.Stat(filepath.Join(projectPath, "src")); os.IsNotExist(err) { + if _, statErr := os.Stat(filepath.Join(projectPath, "src")); os.IsNotExist(statErr) { t.Error("src directory was not created") } - if _, err := os.Stat(filepath.Join(projectPath, "tests")); os.IsNotExist(err) { + if _, statErr := os.Stat(filepath.Join(projectPath, "tests")); os.IsNotExist(statErr) { t.Error("tests directory was not created") } // Check boss.json - bossJsonPath := filepath.Join(projectPath, consts.FilePackage) - if _, err := os.Stat(bossJsonPath); os.IsNotExist(err) { + bossJSONPath := filepath.Join(projectPath, consts.FilePackage) + if _, statErr := os.Stat(bossJSONPath); os.IsNotExist(statErr) { t.Fatal("boss.json was not created") } - bossBytes, err := os.ReadFile(bossJsonPath) + bossBytes, err := os.ReadFile(bossJSONPath) if err != nil { t.Fatalf("Failed to read boss.json: %v", err) } var pkg domain.Package - if err := json.Unmarshal(bossBytes, &pkg); err != nil { + if err = json.Unmarshal(bossBytes, &pkg); err != nil { t.Fatalf("Failed to parse boss.json: %v", err) } @@ -116,7 +112,7 @@ func TestDoCreateProject_App(t *testing.T) { // Check .dpr file dprPath := filepath.Join(projectPath, projectName+".dpr") - if _, err := os.Stat(dprPath); os.IsNotExist(err) { + if _, statErr := os.Stat(dprPath); os.IsNotExist(statErr) { t.Fatal(".dpr file was not created") } @@ -131,7 +127,7 @@ func TestDoCreateProject_App(t *testing.T) { // Check .dproj file dprojPath := filepath.Join(projectPath, projectName+".dproj") - if _, err := os.Stat(dprojPath); os.IsNotExist(err) { + if _, statErr := os.Stat(dprojPath); os.IsNotExist(statErr) { t.Fatal(".dproj file was not created") } @@ -151,15 +147,9 @@ func TestDoCreateProject_App(t *testing.T) { // TestDoCreateProject_Pkg tests bootstrapping a package project. func TestDoCreateProject_Pkg(t *testing.T) { tempDir := t.TempDir() + t.Chdir(tempDir) - // Redirect working directory - oldWd, err := os.Getwd() - if err == nil { - defer func() { _ = os.Chdir(oldWd) }() - } - if err := os.Chdir(tempDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } + var err error // Initialize package manager fs := filesystem.NewOSFileSystem() @@ -172,13 +162,13 @@ func TestDoCreateProject_Pkg(t *testing.T) { doCreateProject(projectName, "pkg", true) projectPath := filepath.Join(tempDir, projectName) - if _, err := os.Stat(projectPath); os.IsNotExist(err) { + if _, statErr := os.Stat(projectPath); os.IsNotExist(statErr) { t.Fatalf("Project directory was not created") } // Check .dpk file dpkPath := filepath.Join(projectPath, projectName+".dpk") - if _, err := os.Stat(dpkPath); os.IsNotExist(err) { + if _, statErr := os.Stat(dpkPath); os.IsNotExist(statErr) { t.Fatal(".dpk file was not created") } @@ -193,7 +183,7 @@ func TestDoCreateProject_Pkg(t *testing.T) { // Check .dproj file dprojPath := filepath.Join(projectPath, projectName+".dproj") - if _, err := os.Stat(dprojPath); os.IsNotExist(err) { + if _, statErr := os.Stat(dprojPath); os.IsNotExist(statErr) { t.Fatal(".dproj file was not created") } diff --git a/internal/core/services/compiler/executor.go b/internal/core/services/compiler/executor.go index cc29034..ae9d80b 100644 --- a/internal/core/services/compiler/executor.go +++ b/internal/core/services/compiler/executor.go @@ -75,11 +75,13 @@ func compileLazarus(lazarusPath string, tracker *BuildTracker) bool { absPath, _ := filepath.Abs(lazarusPath) absDir := filepath.Dir(absPath) - cmd := exec.Command("lazbuild", "--build-mode=Debug", absPath) + // #nosec G204 -- Controlled lazbuild command + cmd := exec.CommandContext(context.Background(), "lazbuild", "--build-mode=Debug", absPath) cmd.Dir = absDir baseName := strings.TrimSuffix(filepath.Base(lazarusPath), filepath.Ext(lazarusPath)) buildLog := filepath.Join(absDir, "build_boss_"+baseName+".log") + // #nosec G304 -- Controlled build log path logFile, err := os.OpenFile(buildLog, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) if err != nil { if tracker == nil || !tracker.IsEnabled() { @@ -87,14 +89,14 @@ func compileLazarus(lazarusPath string, tracker *BuildTracker) bool { } return false } - defer logFile.Close() + defer func() { _ = logFile.Close() }() cmd.Stdout = logFile cmd.Stderr = logFile if err := cmd.Run(); err != nil { if tracker == nil || !tracker.IsEnabled() { - msg.Err(" ❌ Failed to compile, see " + buildLog + " for more information: %v", err) + msg.Err(" ❌ Failed to compile, see "+buildLog+" for more information: %v", err) } return false } @@ -107,7 +109,7 @@ func compileLazarus(lazarusPath string, tracker *BuildTracker) bool { return true } -//nolint:funlen,gocognit,lll // Complex compilation orchestration with long function signature +//nolint:funlen,gocognit,gocyclo,cyclop,lll // Complex compilation orchestration func compile(dprojPath string, dep *domain.Dependency, rootLock domain.PackageLock, tracker *BuildTracker, selectedCompiler *compilerselector.SelectedCompiler) bool { ext := strings.ToLower(filepath.Ext(dprojPath)) if ext == ".lpi" || ext == ".lpk" { diff --git a/internal/core/services/compilerselector/selector.go b/internal/core/services/compilerselector/selector.go index e6052e8..cbce618 100644 --- a/internal/core/services/compilerselector/selector.go +++ b/internal/core/services/compilerselector/selector.go @@ -96,7 +96,10 @@ func resolveTargetPlatform(ctx SelectionContext) string { } } -func findGlobalPathCompiler(installations []registryadapter.DelphiInstallation, globalPath, targetPlatform string) (*SelectedCompiler, error) { +func findGlobalPathCompiler( + installations []registryadapter.DelphiInstallation, + globalPath, targetPlatform string, +) (*SelectedCompiler, error) { for _, inst := range installations { instDir := filepath.Dir(inst.Path) if strings.EqualFold(instDir, globalPath) && strings.EqualFold(inst.Arch, targetPlatform) { @@ -118,7 +121,10 @@ func findGlobalPathCompiler(installations []registryadapter.DelphiInstallation, }, nil } -func findLatestCompiler(installations []registryadapter.DelphiInstallation, targetPlatform string) (*SelectedCompiler, error) { +func findLatestCompiler( + installations []registryadapter.DelphiInstallation, + targetPlatform string, +) (*SelectedCompiler, error) { var bestMatch *registryadapter.DelphiInstallation for _, inst := range installations { if strings.EqualFold(inst.Arch, targetPlatform) { diff --git a/internal/core/services/compilerselector/selector_test.go b/internal/core/services/compilerselector/selector_test.go index f60d22d..f8b5623 100644 --- a/internal/core/services/compilerselector/selector_test.go +++ b/internal/core/services/compilerselector/selector_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashload/boss/pkg/env" ) -// mockRegistryAdapter is a mock for compilerselector.RegistryAdapter +// mockRegistryAdapter is a mock for compilerselector.RegistryAdapter. type mockRegistryAdapter struct { installations []registryadapter.DelphiInstallation } @@ -19,7 +19,7 @@ func (m *mockRegistryAdapter) GetDetectedDelphis() []registryadapter.DelphiInsta return m.installations } -// mockConfigProvider embeds env.ConfigProvider to mock only GetDelphiPath +// mockConfigProvider embeds env.ConfigProvider to mock only GetDelphiPath. type mockConfigProvider struct { env.ConfigProvider delphiPath string diff --git a/internal/core/services/installer/utils.go b/internal/core/services/installer/utils.go index ce8eca2..5b994c4 100644 --- a/internal/core/services/installer/utils.go +++ b/internal/core/services/installer/utils.go @@ -19,7 +19,7 @@ var ( ) // parseURLAndVersion parses the dependency URL and version, handling SSH git@ URLs safely. -func parseURLAndVersion(input string) (url string, version string) { +func parseURLAndVersion(input string) (string, string) { if strings.HasPrefix(input, "git@") { // SSH URL format: git@host:owner/repo[.git][:version] or git@host:owner/repo[.git]@version firstColon := strings.Index(input, ":") @@ -35,8 +35,8 @@ func parseURLAndVersion(input string) (url string, version string) { possibleVersion := remainder[lastSep+1:] // Version shouldn't contain slashes (which paths do) and shouldn't be empty if !strings.Contains(possibleVersion, "/") && possibleVersion != "" { - url = input[:firstColon+1+lastSep] - version = possibleVersion + url := input[:firstColon+1+lastSep] + version := possibleVersion return url, version } } @@ -54,8 +54,8 @@ func parseURLAndVersion(input string) (url string, version string) { } } - url = match["url"] - version = match["version"] + url := match["url"] + version := match["version"] return url, version } diff --git a/internal/core/services/installer/utils_test.go b/internal/core/services/installer/utils_test.go index 44fb6dd..de55056 100644 --- a/internal/core/services/installer/utils_test.go +++ b/internal/core/services/installer/utils_test.go @@ -160,10 +160,10 @@ func TestEnsureDependency_HTTPSUrl(t *testing.T) { func TestEnsureDependency_SSHUrl(t *testing.T) { tests := []struct { - name string - input string - expectedDep string - expectedVer string + name string + input string + expectedDep string + expectedVer string }{ { name: "SSH URL with version tag", @@ -225,4 +225,3 @@ func TestEnsureDependency_SSHUrl(t *testing.T) { }) } } - diff --git a/utils/dcp/dcp_test.go b/utils/dcp/dcp_test.go index d34afaa..756eee1 100644 --- a/utils/dcp/dcp_test.go +++ b/utils/dcp/dcp_test.go @@ -227,4 +227,3 @@ end.` t.Error("injectDcps() should preserve AnotherPkg dependency") } } - diff --git a/utils/librarypath/lazarus_test.go b/utils/librarypath/lazarus_test.go index 111a675..ae29653 100644 --- a/utils/librarypath/lazarus_test.go +++ b/utils/librarypath/lazarus_test.go @@ -55,15 +55,9 @@ const mockLpkContent = ` // TestLazarusPathInjection verifies that other unit search paths are correctly injected in both .lpi and .lpk files. func TestLazarusPathInjection(t *testing.T) { tempDir := t.TempDir() + t.Chdir(tempDir) - // Redirect working directory - oldWd, err := os.Getwd() - if err == nil { - defer func() { _ = os.Chdir(oldWd) }() - } - if err = os.Chdir(tempDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } + var err error // Initialize package manager fs := filesystem.NewOSFileSystem() @@ -141,7 +135,7 @@ func TestLazarusPathInjection(t *testing.T) { // Verify LPK XML contents docLpk := etree.NewDocument() - if err := docLpk.ReadFromFile(lpkPath); err != nil { + if err = docLpk.ReadFromFile(lpkPath); err != nil { t.Fatalf("Failed to read updated LPK: %v", err) } diff --git a/utils/librarypath/librarypath.go b/utils/librarypath/librarypath.go index fd0b1aa..e3e1c20 100644 --- a/utils/librarypath/librarypath.go +++ b/utils/librarypath/librarypath.go @@ -75,14 +75,22 @@ func processBrowsingPath( setReadOnly bool, ) []string { var packagePath = filepath.Join(basePath, value.Name(), consts.FilePackage) - if _, err := os.Stat(packagePath); !os.IsNotExist(err) { - other, err := pkgmanager.LoadPackageOther(packagePath) - if err == nil && other != nil && other.BrowsingPath != "" { - dir := filepath.Join(basePath, value.Name(), other.BrowsingPath) - paths = getNewBrowsingPathsFromDir(dir, paths, fullPath, rootPath) - if setReadOnly { - setReadOnlyProperty(dir) - } + other := loadPackageIfPresent(packagePath) + if other == nil || other.BrowsingPath == "" { + return paths + } + + browsingPaths := strings.Split(other.BrowsingPath, ";") + for _, browsingPath := range browsingPaths { + browsingPath = strings.TrimSpace(browsingPath) + if browsingPath == "" { + continue + } + + dir := filepath.Join(basePath, value.Name(), browsingPath) + paths = getNewBrowsingPathsFromDir(dir, paths, fullPath, rootPath) + if setReadOnly { + setReadOnlyProperty(dir) } } return paths @@ -116,18 +124,52 @@ func GetNewPaths(paths []string, fullPath bool, rootPath string) []string { matches, _ := os.ReadDir(path) for _, value := range matches { - var packagePath = filepath.Join(path, value.Name(), consts.FilePackage) - if _, err := os.Stat(packagePath); !os.IsNotExist(err) { - other, err := pkgmanager.LoadPackageOther(packagePath) - if err == nil && other != nil { - paths = getNewPathsFromDir(filepath.Join(path, value.Name(), other.MainSrc), paths, fullPath, rootPath) - } else { - paths = getNewPathsFromDir(filepath.Join(path, value.Name()), paths, fullPath, rootPath) - } - } else { - paths = getNewPathsFromDir(filepath.Join(path, value.Name()), paths, fullPath, rootPath) + packageDir := filepath.Join(path, value.Name()) + packagePath := filepath.Join(packageDir, consts.FilePackage) + other := loadPackageIfPresent(packagePath) + if other == nil { + paths = getNewPathsFromDir(packageDir, paths, fullPath, rootPath) + continue + } + + paths = appendMainSrcPaths(paths, other, packageDir, fullPath, rootPath) + } + return paths +} + +func loadPackageIfPresent(packagePath string) *domain.Package { + if _, err := os.Stat(packagePath); os.IsNotExist(err) { + return nil + } + + other, err := pkgmanager.LoadPackageOther(packagePath) + if err != nil { + return nil + } + + return other +} + +func appendMainSrcPaths( + paths []string, + pkg *domain.Package, + packageDir string, + fullPath bool, + rootPath string, +) []string { + mainSrcs := strings.Split(pkg.MainSrc, ";") + for _, mainSrc := range mainSrcs { + mainSrc = strings.TrimSpace(mainSrc) + if mainSrc != "" { + paths = getNewPathsFromDir(filepath.Join(packageDir, mainSrc), paths, fullPath, rootPath) + continue + } + + if len(mainSrcs) == 1 { + paths = getNewPathsFromDir(packageDir, paths, fullPath, rootPath) } } + return paths } diff --git a/utils/librarypath/librarypath_test.go b/utils/librarypath/librarypath_test.go index f8f7281..78a4f10 100644 --- a/utils/librarypath/librarypath_test.go +++ b/utils/librarypath/librarypath_test.go @@ -69,3 +69,65 @@ func TestGetNewBrowsingPaths(t *testing.T) { t.Error("GetNewBrowsingPaths() should return paths") } } + +func TestGetNewPaths_MultipleMainSrc(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("BOSS_BASE_DIR", tempDir) + t.Chdir(tempDir) + + var err error + + // Create modules directory + modulesDir := filepath.Join(tempDir, "modules") + libDir := filepath.Join(modulesDir, "my_lib") + err = os.MkdirAll(filepath.Join(libDir, "src"), 0755) + if err != nil { + t.Fatalf("Failed to create src dir: %v", err) + } + err = os.MkdirAll(filepath.Join(libDir, "lib"), 0755) + if err != nil { + t.Fatalf("Failed to create lib dir: %v", err) + } + + // Create a dummy source file inside both src and lib so they are walked and recognized + err = os.WriteFile(filepath.Join(libDir, "src", "my_lib.pas"), []byte("unit my_lib;"), 0600) + if err != nil { + t.Fatalf("Failed to create dummy file: %v", err) + } + err = os.WriteFile(filepath.Join(libDir, "lib", "helper.pas"), []byte("unit helper;"), 0600) + if err != nil { + t.Fatalf("Failed to create dummy file: %v", err) + } + + // Create a boss.json with multiple mainsrc paths + bossJSONContent := `{ + "name": "my_lib", + "mainsrc": "src;lib" + }` + err = os.WriteFile(filepath.Join(libDir, "boss.json"), []byte(bossJSONContent), 0600) + if err != nil { + t.Fatalf("Failed to write boss.json: %v", err) + } + + paths := []string{} + result := GetNewPaths(paths, true, tempDir) + + // Should contain both src and lib paths + foundSrc := false + foundLib := false + for _, p := range result { + if filepath.Base(p) == "src" { + foundSrc = true + } + if filepath.Base(p) == "lib" { + foundLib = true + } + } + + if !foundSrc { + t.Error("Expected to find 'src' path in results") + } + if !foundLib { + t.Error("Expected to find 'lib' path in results") + } +} diff --git a/utils/normalizer/line_normalizer.go b/utils/normalizer/line_normalizer.go index 3acea1b..e5e1478 100644 --- a/utils/normalizer/line_normalizer.go +++ b/utils/normalizer/line_normalizer.go @@ -1,3 +1,4 @@ +// Package normalizer provides line ending normalization for Delphi source files. package normalizer import ( @@ -10,7 +11,7 @@ import ( "github.com/hashload/boss/pkg/msg" ) -var delphiExtensions = map[string]bool{ +var delphiExtensions = map[string]bool{ //nolint:gochecknoglobals // Immutable lookup map ".pas": true, ".inc": true, ".dfm": true, @@ -67,5 +68,6 @@ func NormalizeFileLineEndings(filePath string) error { crlfContent := bytes.ReplaceAll(normalized, []byte("\n"), []byte("\r\n")) // Write back + // #nosec G703 -- Writing Delphi files from controlled directories return os.WriteFile(filePath, crlfContent, 0600) }