diff --git a/cmd/extension/extension_fix.go b/cmd/extension/extension_fix.go index 0b84bfe4..ea6bd364 100644 --- a/cmd/extension/extension_fix.go +++ b/cmd/extension/extension_fix.go @@ -19,7 +19,7 @@ var extensionFixCmd = &cobra.Command{ Use: "fix path", Short: "Apply code-quality fixes to an extension", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(cmd *cobra.Command, args []string) error { allTools := verifier.GetToolsOf[verifier.FixTool]() only, _ := cmd.Flags().GetString("only") exclude, _ := cmd.Flags().GetString("exclude") @@ -37,6 +37,17 @@ var extensionFixCmd = &cobra.Command{ return errors.New("no fixers selected after applying --exclude") } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + allTools := verifier.GetToolsOf[verifier.FixTool]() + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + // Tool selection was validated in PreRunE. + requestedTools, _ := allTools.Only(only) + tools, _ := requestedTools.Exclude(exclude) + allowNonGit, _ := cmd.Flags().GetBool("allow-non-git") if !allowNonGit { @@ -81,6 +92,6 @@ var extensionFixCmd = &cobra.Command{ func init() { extensionRootCmd.AddCommand(extensionFixCmd) extensionFixCmd.Flags().String("only", "", "Run only the specified fixers (comma-separated, e.g. eslint,rector)") - extensionFixCmd.Flags().String("exclude", "", "Skip these fixers; must be in the --only list if set (comma-separated, e.g. eslint,rector)") + extensionFixCmd.Flags().String("exclude", "", "Exclude specified fixers from running (comma-separated, e.g. eslint,rector). When combined with --only, exclude fixers from the selected set") extensionFixCmd.Flags().Bool("allow-non-git", false, "Allow fix to run outside a Git repository") } diff --git a/cmd/extension/extension_format.go b/cmd/extension/extension_format.go index 7211ae66..00d62c8a 100644 --- a/cmd/extension/extension_format.go +++ b/cmd/extension/extension_format.go @@ -19,7 +19,7 @@ var extensionFormat = &cobra.Command{ Use: "format path", Short: "Format an extension's PHP, JavaScript, and SCSS files", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(cmd *cobra.Command, args []string) error { allTools := verifier.GetToolsOf[verifier.FormatTool]() only, _ := cmd.Flags().GetString("only") exclude, _ := cmd.Flags().GetString("exclude") @@ -37,6 +37,17 @@ var extensionFormat = &cobra.Command{ return errors.New("no formatters selected after applying --exclude") } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + allTools := verifier.GetToolsOf[verifier.FormatTool]() + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + // Tool selection was validated in PreRunE. + requestedTools, _ := allTools.Only(only) + tools, _ := requestedTools.Exclude(exclude) + dryRun, _ := cmd.Flags().GetBool("dry-run") path, err := filepath.Abs(args[0]) @@ -75,6 +86,6 @@ var extensionFormat = &cobra.Command{ func init() { extensionRootCmd.AddCommand(extensionFormat) extensionFormat.Flags().String("only", "", "Run only the specified formatters (comma-separated, e.g. prettier,php-cs-fixer)") - extensionFormat.Flags().String("exclude", "", "Skip these formatters; must be in the --only list if set (comma-separated, e.g. prettier,php-cs-fixer)") + extensionFormat.Flags().String("exclude", "", "Exclude specified formatters from running (comma-separated, e.g. prettier,php-cs-fixer). When combined with --only, exclude formatters from the selected set") extensionFormat.Flags().Bool("dry-run", false, "Run in dry run mode") } diff --git a/cmd/extension/extension_validate.go b/cmd/extension/extension_validate.go index 6966d29c..2d117a22 100644 --- a/cmd/extension/extension_validate.go +++ b/cmd/extension/extension_validate.go @@ -24,21 +24,16 @@ var extensionValidateCmd = &cobra.Command{ Long: "Validate an extension folder or ZIP file. With --store-compliance (or SHOPWARE_CLI_STORE_COMPLIANCE=1), the Store's rules apply and the extension's validation.ignore list is not used.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + logger := logging.FromContext(cmd.Context()) storeCompliance, _ := cmd.Flags().GetBool("store-compliance") - reportingFormat, err := extensionValidationFormat(cmd) - if err != nil { - return err - } + // Reporting format and tool selection were validated in PreRunE. + reportingFormat, _ := extensionValidationFormat(cmd) checkAgainst, _ := cmd.Flags().GetString("check-against") only, _ := cmd.Flags().GetString("only") exclude, _ := cmd.Flags().GetString("exclude") noCopy, _ := cmd.Flags().GetBool("no-copy") - verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) - tools, statuses, err := selectExtensionValidationTools(only, exclude) - if err != nil { - return err - } + tools, statuses, _ := selectExtensionValidationTools(only, exclude) needsTools := slices.ContainsFunc(tools, requiresToolSetup) path, err := filepath.Abs(args[0]) @@ -55,7 +50,7 @@ var extensionValidateCmd = &cobra.Command{ if stat.IsDir() { validationPath := path if noCopy { - logging.FromContext(cmd.Context()).Debugf("Skipping copying extension files to temporary directory due to --no-copy flag") + logger.Debugf("Skipping copying extension files to temporary directory due to --no-copy flag") } else if needsTools { tmpDir, err := os.MkdirTemp(os.TempDir(), "analyse-extension-*") if err != nil { @@ -64,9 +59,9 @@ var extensionValidateCmd = &cobra.Command{ defer func() { beforeDeleteTime := time.Now() if err := os.RemoveAll(tmpDir); err != nil { - logging.FromContext(cmd.Context()).Errorf("Failed to remove temporary directory: %v", err) + logger.Errorf("Failed to remove temporary directory: %v", err) } - logging.FromContext(cmd.Context()).Debugf("Removed temporary directory in %s", time.Since(beforeDeleteTime).String()) + logger.Debugf("Removed temporary directory in %s", time.Since(beforeDeleteTime).String()) }() beforeCopyTime := time.Now() @@ -74,7 +69,7 @@ var extensionValidateCmd = &cobra.Command{ return err } - logging.FromContext(cmd.Context()).Debugf("Copied extension files to temporary directory in %s", time.Since(beforeCopyTime).String()) + logger.Debugf("Copied extension files to temporary directory in %s", time.Since(beforeCopyTime).String()) validationPath = tmpDir } @@ -83,7 +78,7 @@ var extensionValidateCmd = &cobra.Command{ return err } - toolCfg, err = verifier.ConvertExtensionToToolConfig(ext) + toolCfg, err = verifier.ConvertExtensionToToolConfig(cmd.Context(), ext) if err != nil { return err } @@ -95,16 +90,16 @@ var extensionValidateCmd = &cobra.Command{ return err } - toolCfg, err = verifier.ConvertExtensionToToolConfig(ext) + toolCfg, err = verifier.ConvertExtensionToToolConfig(cmd.Context(), ext) if err != nil { return err } } if storeCompliance || os.Getenv("SHOPWARE_CLI_STORE_COMPLIANCE") == "1" { - toolCfg.Extension.GetExtensionConfig().Validation.StoreCompliance = true + toolCfg.Extensions[0].GetExtensionConfig().Validation.StoreCompliance = true // The user is not allowed to provide a custom ignore list when store compliance is enabled - toolCfg.Extension.GetExtensionConfig().Validation.Ignore = extension.ConfigValidationList{} + toolCfg.Extensions[0].GetExtensionConfig().Validation.Ignore = extension.ConfigValidationList{} toolCfg.ValidationIgnores = nil } @@ -183,7 +178,7 @@ func init() { extensionValidateCmd.PersistentFlags().String("reporter", "", "Reporting format (summary, json, github, gitlab, junit, markdown)") extensionValidateCmd.PersistentFlags().String("check-against", "highest", "Check against Shopware Version (highest, lowest)") extensionValidateCmd.PersistentFlags().String("only", "", "Run only these validation checks (comma-separated, e.g. phpstan,eslint)") - extensionValidateCmd.PersistentFlags().String("exclude", "", "Exclude specific tools by name (comma-separated, e.g. phpstan,eslint)") + extensionValidateCmd.PersistentFlags().String("exclude", "", "Exclude specified validation checks from running (comma-separated, e.g. phpstan,eslint). When combined with --only, exclude validation checks from the selected set") extensionValidateCmd.PersistentFlags().Bool("no-copy", false, "Do not copy extension files to temporary directory") extensionValidateCmd.MarkFlagsMutuallyExclusive("format", "reporter") _ = extensionValidateCmd.PersistentFlags().MarkDeprecated("full", "all validation checks now run by default; omit --full; to restore old behaviour use --only builtin") @@ -199,6 +194,10 @@ func init() { return fmt.Errorf("invalid --check-against value %q, allowed values: highest, lowest", mode) } - return nil + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) + _, _, err := selectExtensionValidationTools(only, exclude) + return err } } diff --git a/cmd/project/project_fix.go b/cmd/project/project_fix.go index 9a58a0d6..2b78925c 100644 --- a/cmd/project/project_fix.go +++ b/cmd/project/project_fix.go @@ -9,6 +9,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/shopware/shopware-cli/internal/shop" + "github.com/shopware/shopware-cli/internal/validation" "github.com/shopware/shopware-cli/internal/verifier" ) @@ -18,11 +19,23 @@ var projectFixCmd = &cobra.Command{ Long: "Run code-quality fixers on the project's own code, such as extensions in custom/ and configured bundles, and change the files directly. Packages that Composer installs into vendor/ are not changed. Requires a Git repository so the changes can be reviewed, unless --allow-non-git is passed.", Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return verifier.SetupTools(cmd.Context(), cmd.Root().Version) + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) + _, _, err := selectProjectTools(verifier.GetToolsOf[verifier.FixTool](), only, exclude, "fixers") + return err }, RunE: func(cmd *cobra.Command, args []string) error { - var err error + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + // Tool selection was validated in PreRunE. + tools, statuses, _ := selectProjectTools(verifier.GetToolsOf[verifier.FixTool](), only, exclude, "fixers") + if err := verifier.SetupTools(cmd.Context(), cmd.Root().Version); err != nil { + return err + } + var err error projectPath := "" if len(args) > 0 { @@ -46,8 +59,6 @@ var projectFixCmd = &cobra.Command{ } } - only, _ := cmd.Flags().GetString("only") - toolCfg, err := verifier.GetConfigFromProject(cmd.Context(), projectPath, false) if err != nil { return err @@ -55,20 +66,17 @@ var projectFixCmd = &cobra.Command{ var gr errgroup.Group - tools := verifier.GetToolsOf[verifier.FixTool]() - - tools, err = tools.Only(only) - if err != nil { - return err - } - for _, tool := range tools { gr.Go(func() error { return tool.Fix(cmd.Context(), *toolCfg) }) } - return gr.Wait() + runErr := gr.Wait() + if err := validation.PrintToolInvocationTable(os.Stdout, "Fixers", statuses); err != nil { + return err + } + return runErr }, } @@ -76,4 +84,5 @@ func init() { projectRootCmd.AddCommand(projectFixCmd) projectFixCmd.PersistentFlags().String("only", "", "Run only the specified fixers (comma-separated, e.g. eslint,rector)") projectFixCmd.PersistentFlags().Bool("allow-non-git", false, "Allow fix to run outside a Git repository") + projectFixCmd.PersistentFlags().String("exclude", "", "Exclude specified fixers from running (comma-separated, e.g. eslint,rector). When combined with --only, exclude fixers from the selected set") } diff --git a/cmd/project/project_fix_test.go b/cmd/project/project_fix_test.go index b049793d..2ba20019 100644 --- a/cmd/project/project_fix_test.go +++ b/cmd/project/project_fix_test.go @@ -10,6 +10,7 @@ import ( ) func TestProjectFixNoArgsOutsideProjectReturnsError(t *testing.T) { + t.Setenv("SHOPWARE_CLI_TOOLS_DIR", t.TempDir()) chdirOutsideProject(t) projectFixCmd.SetContext(t.Context()) @@ -19,6 +20,7 @@ func TestProjectFixNoArgsOutsideProjectReturnsError(t *testing.T) { } func TestProjectFixNoArgsAppliesGitGuardToResolvedProject(t *testing.T) { + t.Setenv("SHOPWARE_CLI_TOOLS_DIR", t.TempDir()) cwd := t.TempDir() require.NoError(t, os.Mkdir(filepath.Join(cwd, ".git"), 0o755)) t.Chdir(cwd) @@ -34,6 +36,7 @@ func TestProjectFixNoArgsAppliesGitGuardToResolvedProject(t *testing.T) { } func TestProjectFixRejectsNonGitDirectory(t *testing.T) { + t.Setenv("SHOPWARE_CLI_TOOLS_DIR", t.TempDir()) dir := t.TempDir() projectFixCmd.SetContext(t.Context()) err := projectFixCmd.RunE(projectFixCmd, []string{dir}) @@ -43,6 +46,7 @@ func TestProjectFixRejectsNonGitDirectory(t *testing.T) { } func TestProjectFixPassesGitGuardWithGitDirectory(t *testing.T) { + t.Setenv("SHOPWARE_CLI_TOOLS_DIR", t.TempDir()) dir := t.TempDir() require.NoError(t, os.Mkdir(filepath.Join(dir, ".git"), 0o755)) diff --git a/cmd/project/project_format.go b/cmd/project/project_format.go index df64c810..73a87b11 100644 --- a/cmd/project/project_format.go +++ b/cmd/project/project_format.go @@ -2,12 +2,14 @@ package project import ( "fmt" + "os" "path/filepath" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" "github.com/shopware/shopware-cli/internal/shop" + "github.com/shopware/shopware-cli/internal/validation" "github.com/shopware/shopware-cli/internal/verifier" ) @@ -17,13 +19,24 @@ var projectFormatCmd = &cobra.Command{ Long: "Format the project's own code, such as extensions in custom/ and configured bundles, and change the files directly. Packages that Composer installs into vendor/ are not changed. PHP-CS-Fixer uses the project's .php-cs-fixer.dist.php if present; Prettier always uses the CLI's own config. Use --dry-run to only report files that would change.", Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return verifier.SetupTools(cmd.Context(), cmd.Root().Version) + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) + _, _, err := selectProjectTools(verifier.GetToolsOf[verifier.FormatTool](), only, exclude, "formatters") + return err }, RunE: func(cmd *cobra.Command, args []string) error { - var err error only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + // Tool selection was validated in PreRunE. + tools, statuses, _ := selectProjectTools(verifier.GetToolsOf[verifier.FormatTool](), only, exclude, "formatters") + if err := verifier.SetupTools(cmd.Context(), cmd.Root().Version); err != nil { + return err + } dryRun, _ := cmd.Flags().GetBool("dry-run") + var err error projectPath := "" if len(args) > 0 { @@ -47,20 +60,17 @@ var projectFormatCmd = &cobra.Command{ var gr errgroup.Group - tools := verifier.GetToolsOf[verifier.FormatTool]() - - tools, err = tools.Only(only) - if err != nil { - return err - } - for _, tool := range tools { gr.Go(func() error { return tool.Format(cmd.Context(), *toolCfg, dryRun) }) } - return gr.Wait() + runErr := gr.Wait() + if err := validation.PrintToolInvocationTable(os.Stdout, "Formatters", statuses); err != nil { + return err + } + return runErr }, } @@ -68,4 +78,5 @@ func init() { projectRootCmd.AddCommand(projectFormatCmd) projectFormatCmd.PersistentFlags().String("only", "", "Run only the specified formatters (comma-separated, e.g. prettier,php-cs-fixer)") projectFormatCmd.PersistentFlags().Bool("dry-run", false, "Report files that would change, without changing them") + projectFormatCmd.PersistentFlags().String("exclude", "", "Exclude specified formatters from running (comma-separated, e.g. prettier,php-cs-fixer). When combined with --only, exclude formatters from the selected set") } diff --git a/cmd/project/project_tool_invocation.go b/cmd/project/project_tool_invocation.go new file mode 100644 index 00000000..5082fbce --- /dev/null +++ b/cmd/project/project_tool_invocation.go @@ -0,0 +1,42 @@ +package project + +import ( + "fmt" + "slices" + "sort" + + "github.com/shopware/shopware-cli/internal/validation" + "github.com/shopware/shopware-cli/internal/verifier" +) + +func projectToolInvocationStatuses[T verifier.Tool](all, requested, selected verifier.ToolList[T]) []validation.ToolInvocationStatus { + statuses := make([]validation.ToolInvocationStatus, 0, len(all)) + for _, tool := range all { + status := validation.ToolInvocationStatus{Name: tool.Name(), Status: "skipped", Reason: "not selected by --only"} + switch { + case slices.ContainsFunc(selected, func(selected T) bool { return selected.Name() == tool.Name() }): + status.Status = "invoked" + status.Reason = "" + case slices.ContainsFunc(requested, func(requested T) bool { return requested.Name() == tool.Name() }): + status.Reason = "excluded by --exclude" + } + statuses = append(statuses, status) + } + sort.Slice(statuses, func(i, j int) bool { return statuses[i].Name < statuses[j].Name }) + return statuses +} + +func selectProjectTools[T verifier.Tool](all verifier.ToolList[T], only, exclude, kind string) (verifier.ToolList[T], []validation.ToolInvocationStatus, error) { + requested, err := all.Only(only) + if err != nil { + return nil, nil, err + } + selected, err := requested.Exclude(exclude) + if err != nil { + return nil, nil, err + } + if len(selected) == 0 { + return nil, nil, fmt.Errorf("no %s selected after applying --exclude", kind) + } + return selected, projectToolInvocationStatuses(all, requested, selected), nil +} diff --git a/cmd/project/project_validate.go b/cmd/project/project_validate.go index 34eab722..cdd44217 100644 --- a/cmd/project/project_validate.go +++ b/cmd/project/project_validate.go @@ -24,19 +24,27 @@ var projectValidateCmd = &cobra.Command{ if _, err := projectValidationFormat(cmd); err != nil { return err } - return verifier.SetupTools(cmd.Context(), cmd.Root().Version) + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) + _, _, err := selectProjectTools(verifier.GetToolsOf[verifier.CheckTool](), only, exclude, "validation checks") + return err }, RunE: func(cmd *cobra.Command, args []string) error { - reportingFormat, err := projectValidationFormat(cmd) - if err != nil { - return err - } + // Reporting format and tool selection were validated in PreRunE. + reportingFormat, _ := projectValidationFormat(cmd) only, _ := cmd.Flags().GetString("only") exclude, _ := cmd.Flags().GetString("exclude") - verifier.WarnOnDeprecatedToolName(cmd.Context(), only, exclude) + + checkers := verifier.GetToolsOf[verifier.CheckTool]() + tools, statuses, _ := selectProjectTools(checkers, only, exclude, "validation checks") + if err := verifier.SetupTools(cmd.Context(), cmd.Root().Version); err != nil { + return err + } noCopy, _ := cmd.Flags().GetBool("no-copy") localOnly, _ := cmd.Flags().GetBool("local-only") + var err error projectPath := "" if len(args) > 0 { @@ -81,31 +89,18 @@ var projectValidateCmd = &cobra.Command{ var gr errgroup.Group - tools := verifier.GetToolsOf[verifier.CheckTool]() - - tools, err = tools.Only(only) - if err != nil { - return err - } - - tools, err = tools.Exclude(exclude) - if err != nil { - return err - } - for _, tool := range tools { gr.Go(func() error { return tool.Check(cmd.Context(), result, *toolCfg) }) } - if err := gr.Wait(); err != nil { - return err + runErr := gr.Wait() + reportErr := validation.DoCheckReport(result.RemoveByIdentifier(toolCfg.ValidationIgnores), reportingFormat, runErr != nil, statuses...) + if runErr != nil { + return runErr } - - filtered := result.RemoveByIdentifier(toolCfg.ValidationIgnores) - - return validation.DoCheckReport(filtered, reportingFormat, false) + return reportErr }, } @@ -126,8 +121,8 @@ func init() { projectRootCmd.AddCommand(projectValidateCmd) projectValidateCmd.PersistentFlags().String("format", "", "Report format (summary, json, github, gitlab, junit, markdown; auto-detected if unset)") projectValidateCmd.PersistentFlags().String("reporter", "", "Reporting format (summary, json, github, gitlab, junit, markdown)") - projectValidateCmd.PersistentFlags().String("only", "", "Run only the specified tools (comma-separated). Available: phpstan, eslint, stylelint, storefront-twig, builtin (legacy alias: sw-cli, deprecated)") - projectValidateCmd.PersistentFlags().String("exclude", "", "Skip these tools (comma-separated); with --only, each must be selected there. Names: phpstan, eslint, stylelint, storefront-twig, builtin (legacy alias: sw-cli, deprecated)") + projectValidateCmd.PersistentFlags().String("only", "", "Run only these validation checks (comma-separated, e.g. phpstan,eslint)") + projectValidateCmd.PersistentFlags().String("exclude", "", "Exclude specified validation checks from running (comma-separated, e.g. phpstan,eslint). When combined with --only, exclude validation checks from the selected set") projectValidateCmd.PersistentFlags().Bool("no-copy", false, "Validate the project directory itself, not a temporary copy") projectValidateCmd.PersistentFlags().Bool("local-only", false, "Validate only extensions in custom/* folders") projectValidateCmd.MarkFlagsMutuallyExclusive("format", "reporter") diff --git a/internal/extension/project.go b/internal/extension/project.go index 9ab48a2a..d81b7b1c 100644 --- a/internal/extension/project.go +++ b/internal/extension/project.go @@ -265,7 +265,7 @@ func FindExtensionsFromProject(ctx context.Context, project string, onlyLocal bo version, _ := ext.GetVersion() - logging.FromContext(ctx).Infof("Found extension in custom/plugins: %s (%s)", name, version) + logging.FromContext(ctx).Infof("Found extension in custom/static-plugins: %s (%s)", name, version) extensions[name] = ext } diff --git a/internal/verifier/builtin.go b/internal/verifier/builtin.go index 0deea39f..e68fed7e 100644 --- a/internal/verifier/builtin.go +++ b/internal/verifier/builtin.go @@ -2,6 +2,7 @@ package verifier import ( "context" + "path/filepath" "github.com/shopware/shopware-cli/internal/extension" "github.com/shopware/shopware-cli/internal/validation" @@ -14,31 +15,44 @@ func (s Builtin) Name() string { } func (s Builtin) Check(ctx context.Context, check *Check, config ToolConfig) error { - if config.Extension == nil { - return nil - } - - extension.RunValidation(ctx, config.Extension, check) - - // Apply ignores from extension config ignores := make([]validation.ToolConfigIgnore, 0) - for _, ignore := range config.Extension.GetExtensionConfig().Validation.Ignore { - ignores = append(ignores, validation.ToolConfigIgnore{ - Identifier: ignore.Identifier, - Path: ignore.Path, - Message: ignore.Message, - }) - } - - if config.InputWasDirectory { - // Add additional ignores for directory input - ignores = append(ignores, validation.ToolConfigIgnore{ - Identifier: "zip.disallowed_file", - }) - } - if len(ignores) > 0 { - check.RemoveByIdentifier(ignores) + for _, ext := range config.Extensions { + extensionCheck := NewCheck() + extensionCheck.SetSourceRoot(ext.GetPath()) + extension.RunValidation(ctx, ext, extensionCheck) + + // Apply ignores from extension config + ignores = ignores[:0] // rebuild ignores for each extension + for _, ignore := range ext.GetExtensionConfig().Validation.Ignore { + ignores = append(ignores, validation.ToolConfigIgnore{ + Identifier: ignore.Identifier, + Path: ignore.Path, + Message: ignore.Message, + }) + } + + if config.InputWasDirectory { + // Add additional ignores for directory input + ignores = append(ignores, validation.ToolConfigIgnore{ + Identifier: "zip.disallowed_file", + }) + } + + if len(ignores) > 0 { + extensionCheck.RemoveByIdentifier(ignores) + } + for _, result := range extensionCheck.GetResults() { + // Builtin validators report extension-relative paths. Rebase them + // before merging into the project-wide check. + if result.Path != "" && config.RootDir != "" { + if !filepath.IsAbs(result.Path) { + result.Path = filepath.Join(ext.GetPath(), result.Path) + } + result.Path = validation.NormalizeSourcePath(result.Path, config.RootDir) + } + check.AddResult(result) + } } return nil diff --git a/internal/verifier/builtin_test.go b/internal/verifier/builtin_test.go new file mode 100644 index 00000000..531759a2 --- /dev/null +++ b/internal/verifier/builtin_test.go @@ -0,0 +1,64 @@ +package verifier + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/shopware/shopware-cli/internal/extension" + "github.com/shopware/shopware-cli/internal/testhelper" + "github.com/shopware/shopware-cli/internal/validation" +) + +func TestBuiltinScopesExtensionIgnores(t *testing.T) { + for _, ignorePath := range []string{"", "src/Resources/config/services.xml"} { + for _, ignoredFirst := range []bool{false, true} { + t.Run(fmt.Sprintf("path=%s/ignored-first=%t", ignorePath, ignoredFirst), func(t *testing.T) { + p := testhelper.NewProject(t) + var exts []extension.Extension + for _, name := range []string{"FirstPlugin", "SecondPlugin"} { + p.CustomPlugin(name, testhelper.PluginComposer("test/"+name, "1.0.0", name+`\`+name)) + pluginDir := filepath.Join(p.Root, "custom", "plugins", name) + writeDeprecatedServicesXML(t, pluginDir) + ext, err := extension.GetExtensionByFolder(t.Context(), pluginDir) + require.NoError(t, err) + exts = append(exts, ext) + } + exts[0].GetExtensionConfig().Validation.Ignore = extension.ConfigValidationList{{ + Identifier: "config.services_xml.deprecated", + Path: ignorePath, + }} + if !ignoredFirst { + exts[0], exts[1] = exts[1], exts[0] + } + + check := NewCheck() + check.SetSourceRoot(p.Root) + previous := validation.CheckResult{ + Identifier: "config.services_xml.deprecated", + Path: "src/Resources/config/services.xml", + Line: 1, + } + check.AddResult(previous) + require.NoError(t, Builtin{}.Check(t.Context(), check, ToolConfig{ + Extensions: exts, + RootDir: p.Root, + })) + + var paths []string + for _, result := range check.GetResults() { + if result.Identifier == previous.Identifier { + paths = append(paths, result.Path) + } + } + assert.ElementsMatch(t, []string{ + previous.Path, + "custom/plugins/SecondPlugin/src/Resources/config/services.xml", + }, paths) + }) + } + } +} diff --git a/internal/verifier/composer.go b/internal/verifier/composer.go index b62ac2f9..37c4ea90 100644 --- a/internal/verifier/composer.go +++ b/internal/verifier/composer.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path" "time" @@ -33,7 +32,7 @@ func installComposerDeps(ctx context.Context, rootDir string, checkAgainst strin additionalParams = append(additionalParams, suggest+":*") } - composerInstall := exec.CommandContext(ctx, "composer", additionalParams...) + composerInstall := CommandContext(ctx, "composer", additionalParams...) composerInstall.Env = append(os.Environ(), fmt.Sprintf("COMPOSER_AUTH=%s", encoded)) composerInstall.Dir = rootDir @@ -52,7 +51,7 @@ func installComposerDeps(ctx context.Context, rootDir string, checkAgainst strin additionalParams = append(additionalParams, "--prefer-lowest") } - composerInstall := exec.CommandContext(ctx, "composer", additionalParams...) + composerInstall := CommandContext(ctx, "composer", additionalParams...) composerInstall.Env = append(os.Environ(), fmt.Sprintf("COMPOSER_AUTH=%s", encoded)) composerInstall.Dir = rootDir diff --git a/internal/verifier/eslint.go b/internal/verifier/eslint.go index a26db960..5f08315c 100644 --- a/internal/verifier/eslint.go +++ b/internal/verifier/eslint.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path" "golang.org/x/sync/errgroup" @@ -58,7 +57,7 @@ func (e Eslint) Check(ctx context.Context, check *Check, config ToolConfig) erro p := p gr.Go(func() error { - eslint := exec.CommandContext(ctx, + eslint := CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "eslint"), "--format=json", @@ -119,7 +118,7 @@ func (e Eslint) Fix(ctx context.Context, config ToolConfig) error { p := p gr.Go(func() error { - eslint := exec.CommandContext(ctx, + eslint := CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "eslint"), "--config", path.Join(config.ToolDirectory, "js", "configs", fmt.Sprintf("eslint.config.%s.mjs", path.Base(p))), diff --git a/internal/verifier/extension.go b/internal/verifier/extension.go index 42825185..e24b5fb4 100644 --- a/internal/verifier/extension.go +++ b/internal/verifier/extension.go @@ -16,10 +16,10 @@ func SetupExtensionToolConfig(ctx context.Context, version string, ext extension if err := SetupTools(ctx, version); err != nil { return nil, err } - return ConvertExtensionToToolConfig(ext) + return ConvertExtensionToToolConfig(ctx, ext) } -func ConvertExtensionToToolConfig(ext extension.Extension) (*ToolConfig, error) { +func ConvertExtensionToToolConfig(ctx context.Context, ext extension.Extension) (*ToolConfig, error) { var ignores []validation.ToolConfigIgnore for _, ignore := range ext.GetExtensionConfig().Validation.Ignore { @@ -32,7 +32,7 @@ func ConvertExtensionToToolConfig(ext extension.Extension) (*ToolConfig, error) cfg := &ToolConfig{ ToolDirectory: GetToolDirectory(), - Extension: ext, + Extensions: []extension.Extension{ext}, ValidationIgnores: ignores, RootDir: ext.GetPath(), SourceDirectories: ext.GetSourceDirs(), @@ -49,6 +49,7 @@ func ConvertExtensionToToolConfig(ext extension.Extension) (*ToolConfig, error) return nil, err } + cfg.logConfiguration(ctx) return cfg, nil } diff --git a/internal/verifier/phpcsfixer.go b/internal/verifier/phpcsfixer.go index fd5b4f3c..992e9e4d 100644 --- a/internal/verifier/phpcsfixer.go +++ b/internal/verifier/phpcsfixer.go @@ -3,7 +3,6 @@ package verifier import ( "context" "os" - "os/exec" "path" "golang.org/x/sync/errgroup" @@ -40,7 +39,7 @@ func (p PHPCSFixer) Format(ctx context.Context, config ToolConfig, dryRun bool) args = append(args, "--dry-run") } - cmd := exec.CommandContext(ctx, path.Join(config.ToolDirectory, "php", "vendor", "bin", "php-cs-fixer"), args...) + cmd := CommandContext(ctx, path.Join(config.ToolDirectory, "php", "vendor", "bin", "php-cs-fixer"), args...) cmd.Dir = config.RootDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/verifier/phpstan.go b/internal/verifier/phpstan.go index 427f2cd7..4f2a36ee 100644 --- a/internal/verifier/phpstan.go +++ b/internal/verifier/phpstan.go @@ -6,7 +6,6 @@ import ( _ "embed" "encoding/json" "os" - "os/exec" "path" "regexp" "strings" @@ -65,7 +64,6 @@ func (p PhpStan) Check(ctx context.Context, check *Check, config ToolConfig) err if err := installComposerDeps(ctx, config.RootDir, config.CheckAgainst); err != nil { return err } - for _, sourceDirectory := range config.SourceDirectories { phpstanArguments := []string{"-dmemory_limit=2G", path.Join(config.ToolDirectory, "php", "vendor", "bin", "phpstan"), "analyse", "--no-progress", "--no-interaction", "--error-format=json", sourceDirectory} @@ -77,7 +75,7 @@ func (p PhpStan) Check(ctx context.Context, check *Check, config ToolConfig) err phpstanArguments = append(phpstanArguments, "-v") } - phpstan := exec.CommandContext(ctx, "php", phpstanArguments...) + phpstan := CommandContext(ctx, "php", phpstanArguments...) phpstan.Env = append(os.Environ(), "PHP_DIR="+path.Join(config.ToolDirectory, "php")) phpstan.Dir = config.RootDir diff --git a/internal/verifier/prettier.go b/internal/verifier/prettier.go index 40bd693b..358f74bf 100644 --- a/internal/verifier/prettier.go +++ b/internal/verifier/prettier.go @@ -3,7 +3,6 @@ package verifier import ( "context" "os" - "os/exec" "path" "golang.org/x/sync/errgroup" @@ -49,7 +48,7 @@ func (b Prettier) Format(ctx context.Context, config ToolConfig, dryRun bool) er } gr.Go(func() error { - cmd := exec.CommandContext(ctx, "node", args...) + cmd := CommandContext(ctx, "node", args...) cmd.Dir = sourceDirectory cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout diff --git a/internal/verifier/project.go b/internal/verifier/project.go index d699598f..497073ed 100644 --- a/internal/verifier/project.go +++ b/internal/verifier/project.go @@ -96,7 +96,7 @@ func GetConfigFromProject(ctx context.Context, root string, onlyLocal bool) (*To adminDirectories := []string{} storefrontDirectories := []string{} - vendorPath := path.Join(root, "vendor") + vendorPath := validation.ResolveSourceRoot(path.Join(root, "vendor")) actualProjectConfigPath := shop.SearchConfigPath(ctx, root, "") shopCfg, err := shop.ReadConfig(ctx, actualProjectConfigPath, true) @@ -112,27 +112,25 @@ func GetConfigFromProject(ctx context.Context, root string, onlyLocal bool) (*To } } + checkExtensions := []extension.Extension{} + for _, ext := range extensions { extName, err := ext.GetName() if err != nil { return nil, err } - rootDir := ext.GetRootDir() - - resolvedPath, err := filepath.EvalSymlinks(rootDir) - if err == nil { - rootDir = resolvedPath - } + rootDir := validation.ResolveSourceRoot(ext.GetRootDir()) // Skip plugins in vendor folder - if strings.HasPrefix(rootDir, vendorPath) || slices.Contains(excludeExtensions, extName) { + if (rootDir == vendorPath || strings.HasPrefix(rootDir, vendorPath+string(filepath.Separator))) || slices.Contains(excludeExtensions, extName) { continue } sourceDirectories = append(sourceDirectories, ext.GetSourceDirs()...) adminDirectories = append(adminDirectories, getAdminFolders(ext)...) storefrontDirectories = append(storefrontDirectories, getStorefrontFolders(ext)...) + checkExtensions = append(checkExtensions, ext) } var rootComposerJsonData rootComposerJson @@ -206,17 +204,20 @@ func GetConfigFromProject(ctx context.Context, root string, onlyLocal bool) (*To toolCfg := &ToolConfig{ ToolDirectory: GetToolDirectory(), + InputWasDirectory: true, RootDir: root, SourceDirectories: sourceDirectories, AdminDirectories: adminDirectories, StorefrontDirectories: storefrontDirectories, ValidationIgnores: validationIgnores, + Extensions: checkExtensions, } if err := determineVersionRange(toolCfg, constraint); err != nil { return nil, err } + toolCfg.logConfiguration(ctx) return toolCfg, nil } diff --git a/internal/verifier/project_test.go b/internal/verifier/project_test.go index 73f24341..37bbc8c0 100644 --- a/internal/verifier/project_test.go +++ b/internal/verifier/project_test.go @@ -2,14 +2,60 @@ package verifier import ( "context" + "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/shopware/shopware-cli/internal/testhelper" ) +func TestProjectBuiltinValidatesOnlyIncludedLocalExtensions(t *testing.T) { + stubShopwareVersions(t) + p := testhelper.NewProject(t). + File("composer.json", testProjectComposerJSON.String()). + File(".shopware-project.yml", "validation:\n ignore_extensions:\n - name: IgnoredPlugin\n") + for _, name := range []string{"LocalPlugin", "IgnoredPlugin"} { + p.CustomPlugin(name, testhelper.PluginComposer("test/"+name, "1.0.0", name+`\`+name)) + pluginDir := filepath.Join(p.Root, "custom", "plugins", name) + writeDeprecatedServicesXML(t, pluginDir) + testhelper.WriteFile(t, filepath.Join(pluginDir, ".DS_Store"), "store") + } + // Vendor symlinks are excluded; a sibling with a similar name is local code. + for _, dir := range []string{"vendor", "vendor-local"} { + name := "VendorPlugin" + if dir == "vendor-local" { + name = "SiblingPlugin" + } + pluginDir := filepath.Join(p.Root, dir, "test", name) + testhelper.WriteFile(t, filepath.Join(pluginDir, "composer.json"), testhelper.PluginComposer("test/"+name, "1.0.0", name+`\`+name).String()) + writeDeprecatedServicesXML(t, pluginDir) + require.NoError(t, os.Symlink(pluginDir, filepath.Join(p.Root, "custom", "plugins", name))) + } + + cfg, err := GetConfigFromProject(t.Context(), p.Root, true) + require.NoError(t, err) + assert.True(t, cfg.InputWasDirectory) + require.Len(t, cfg.Extensions, 2) + + check := NewCheck() + check.SetSourceRoot(p.Root) + require.NoError(t, Builtin{}.Check(t.Context(), check, *cfg)) + var xmlPaths []string + for _, result := range check.GetResults() { + assert.NotEqual(t, "zip.disallowed_file", result.Identifier) + if result.Identifier == "config.services_xml.deprecated" { + xmlPaths = append(xmlPaths, result.Path) + } + } + assert.ElementsMatch(t, []string{ + "custom/plugins/LocalPlugin/src/Resources/config/services.xml", + "vendor-local/test/SiblingPlugin/src/Resources/config/services.xml", + }, xmlPaths) +} + // stubShopwareVersions replaces the network-backed version lookup with a // fixed list, so GetConfigFromProject does not hit repo.packagist.org. func stubShopwareVersions(t *testing.T) { diff --git a/internal/verifier/rector.go b/internal/verifier/rector.go index 00a3b4f2..8f88d453 100644 --- a/internal/verifier/rector.go +++ b/internal/verifier/rector.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "os/exec" "path" ) @@ -64,7 +63,7 @@ func (r Rector) Fix(ctx context.Context, config ToolConfig) error { } for _, sourceDirectory := range config.SourceDirectories { - rector := exec.CommandContext(ctx, "php", "-dmemory_limit=2G", path.Join(config.ToolDirectory, "php", "vendor", "bin", "rector"), "process", "--config", rectorConfigFile, "--autoload-file", path.Join("vendor", "autoload.php"), sourceDirectory) + rector := CommandContext(ctx, "php", "-dmemory_limit=2G", path.Join(config.ToolDirectory, "php", "vendor", "bin", "rector"), "process", "--config", rectorConfigFile, "--autoload-file", path.Join("vendor", "autoload.php"), sourceDirectory) rector.Dir = config.RootDir log, _ := rector.CombinedOutput() diff --git a/internal/verifier/source_location_test.go b/internal/verifier/source_location_test.go index 9a11f740..34b9bbc1 100644 --- a/internal/verifier/source_location_test.go +++ b/internal/verifier/source_location_test.go @@ -32,8 +32,8 @@ func TestZipValidationUsesArchiveRelativePaths(t *testing.T) { check := NewCheck() check.SetSourceRoot(ext.GetPath()) require.NoError(t, Builtin{}.Check(t.Context(), check, ToolConfig{ - Extension: ext, - RootDir: ext.GetPath(), + Extensions: []extension.Extension{ext}, + RootDir: ext.GetPath(), })) results := check.GetResults() @@ -78,7 +78,7 @@ func TestDirectoryValidationUsesExtensionRelativePaths(t *testing.T) { check := NewCheck() check.SetSourceRoot(pluginDir) require.NoError(t, Builtin{}.Check(t.Context(), check, ToolConfig{ - Extension: ext, + Extensions: []extension.Extension{ext}, RootDir: pluginDir, InputWasDirectory: true, })) diff --git a/internal/verifier/stylelint.go b/internal/verifier/stylelint.go index 4fd25079..7f0c4503 100644 --- a/internal/verifier/stylelint.go +++ b/internal/verifier/stylelint.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path" "path/filepath" "strings" @@ -57,7 +56,7 @@ func (s StyleLint) Check(ctx context.Context, check *Check, config ToolConfig) e } gr.Go(func() error { - stylelint := exec.CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "stylelint"), + stylelint := CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "stylelint"), "--formatter=json", "--config", path.Join(config.ToolDirectory, "js", "configs", fmt.Sprintf("stylelint.config.%s.mjs", path.Base(p))), "--ignore-pattern", "dist/**", @@ -125,7 +124,7 @@ func (s StyleLint) Fix(ctx context.Context, config ToolConfig) error { } gr.Go(func() error { - stylelint := exec.CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "stylelint"), + stylelint := CommandContext(ctx, "node", path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "stylelint"), "--config", path.Join(config.ToolDirectory, "js", "configs", fmt.Sprintf("stylelint.config.%s.mjs", path.Base(p))), "--ignore-pattern", "dist/**", "--ignore-pattern", ".tmp/**", diff --git a/internal/verifier/symfony_xml.go b/internal/verifier/symfony_xml.go index edede2b4..498a8f7a 100644 --- a/internal/verifier/symfony_xml.go +++ b/internal/verifier/symfony_xml.go @@ -4,7 +4,6 @@ import ( "context" "os" "path/filepath" - "strings" "github.com/shopware/shopware-cli/internal/extension" "github.com/shopware/shopware-cli/internal/symfony" @@ -29,7 +28,7 @@ func (s SymfonyXMLConverter) Fix(ctx context.Context, config ToolConfig) error { {"routes.xml", symfony.ConvertRoutesXMLFile}, } - for _, configDir := range s.collectConfigDirs(ctx, config) { + for _, configDir := range s.collectConfigDirs(config) { for _, conversion := range conversions { xmlPath := filepath.Join(configDir, conversion.fileName) @@ -57,12 +56,12 @@ func (s SymfonyXMLConverter) Fix(ctx context.Context, config ToolConfig) error { // container and bundles may load their configuration files explicitly by // path, so only plugins (where Shopware picks the XML or YAML variant // automatically) are converted. -func (SymfonyXMLConverter) collectConfigDirs(ctx context.Context, config ToolConfig) []string { +func (SymfonyXMLConverter) collectConfigDirs(config ToolConfig) []string { dirs := []string{} - addExtension := func(ext extension.Extension) { + for _, ext := range config.Extensions { if ext.GetType() != extension.TypePlatformPlugin { - return + continue } for _, resourcesDir := range ext.GetResourcesDirs() { @@ -76,27 +75,6 @@ func (SymfonyXMLConverter) collectConfigDirs(ctx context.Context, config ToolCon } } - if config.Extension != nil { - addExtension(config.Extension) - - return dirs - } - - vendorDir := filepath.Join(config.RootDir, "vendor") - - for _, ext := range extension.FindExtensionsFromProject(logging.DisableLogger(ctx), config.RootDir, true) { - rootDir := ext.GetRootDir() - if resolvedPath, err := filepath.EvalSymlinks(rootDir); err == nil { - rootDir = resolvedPath - } - - if strings.HasPrefix(rootDir, vendorDir) { - continue - } - - addExtension(ext) - } - return dirs } diff --git a/internal/verifier/symfony_xml_test.go b/internal/verifier/symfony_xml_test.go index 1a36e439..5a148acd 100644 --- a/internal/verifier/symfony_xml_test.go +++ b/internal/verifier/symfony_xml_test.go @@ -57,7 +57,7 @@ func TestSymfonyXMLConverterFixesPlugin(t *testing.T) { ext, err := extension.GetExtensionByFolder(t.Context(), tmpDir) require.NoError(t, err) - err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extension: ext, RootDir: tmpDir}) + err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extensions: []extension.Extension{ext}, RootDir: tmpDir}) require.NoError(t, err) assertConvertedConfigDir(t, filepath.Join(tmpDir, "src", "Resources", "config")) @@ -70,7 +70,7 @@ func TestSymfonyXMLConverterFixesExtraBundles(t *testing.T) { ext, err := extension.GetExtensionByFolder(t.Context(), tmpDir) require.NoError(t, err) - err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extension: ext, RootDir: tmpDir}) + err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extensions: []extension.Extension{ext}, RootDir: tmpDir}) require.NoError(t, err) assert.NoFileExists(t, filepath.Join(bundleConfigDir, "services.xml")) @@ -87,7 +87,7 @@ func TestSymfonyXMLConverterSkipsApps(t *testing.T) { ext, err := extension.GetExtensionByFolder(t.Context(), tmpDir) require.NoError(t, err) - err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extension: ext, RootDir: tmpDir}) + err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extensions: []extension.Extension{ext}, RootDir: tmpDir}) require.NoError(t, err) assert.FileExists(t, filepath.Join(configDir, "services.xml")) @@ -103,7 +103,7 @@ func TestSymfonyXMLConverterKeepsUnconvertibleFile(t *testing.T) { ext, err := extension.GetExtensionByFolder(t.Context(), tmpDir) require.NoError(t, err) - err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extension: ext, RootDir: tmpDir}) + err = SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{Extensions: []extension.Extension{ext}, RootDir: tmpDir}) require.NoError(t, err) assert.FileExists(t, filepath.Join(configDir, "services.xml")) @@ -113,7 +113,9 @@ func TestSymfonyXMLConverterKeepsUnconvertibleFile(t *testing.T) { func TestSymfonyXMLConverterFixesProjectPlugins(t *testing.T) { tmpDir := copySymfonyXMLFixture(t, "project") - err := SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{RootDir: tmpDir}) + exts := extension.FindExtensionsFromProject(t.Context(), tmpDir, true) + require.NotEmpty(t, exts) + err := SymfonyXMLConverter{}.Fix(t.Context(), ToolConfig{RootDir: tmpDir, Extensions: exts}) require.NoError(t, err) assertConvertedConfigDir(t, filepath.Join(tmpDir, "custom", "plugins", "TestPlugin", "src", "Resources", "config")) diff --git a/internal/verifier/tool.go b/internal/verifier/tool.go index 74df76a6..273a95da 100644 --- a/internal/verifier/tool.go +++ b/internal/verifier/tool.go @@ -3,6 +3,7 @@ package verifier import ( "context" "fmt" + "os/exec" "slices" "strings" @@ -56,8 +57,30 @@ type ToolConfig struct { AdminDirectories []string // Contains a list of directories that are considered as storefront code StorefrontDirectories []string + // List of extensions of the project or a single extension + Extensions []extension.Extension +} - Extension extension.Extension +func (c *ToolConfig) logConfiguration(ctx context.Context) { + logger := logging.FromContext(ctx) + logger.Debugf("Tool root directory: %s", c.RootDir) + for _, dir := range c.SourceDirectories { + logger.Debugf("Tool source directory: %s", dir) + } + for _, dir := range c.AdminDirectories { + logger.Debugf("Tool administration directory: %s", dir) + } + for _, dir := range c.StorefrontDirectories { + logger.Debugf("Tool storefront directory: %s", dir) + } + for _, ext := range c.Extensions { + name, err := ext.GetName() + if err != nil { + logger.Debugf("Cannot read tool extension name for %s: %v", ext.GetPath(), err) + continue + } + logger.Debugf("Tool extension: %s", name) + } } type Tool interface { @@ -168,3 +191,9 @@ func (tl ToolList[T]) PossibleString() string { return strings.Join(possibleTools, ",") } + +// CommandContext logs the command before creating it with exec.CommandContext. +func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd { + logging.FromContext(ctx).Debugf("Executing command: %s %s", name, strings.Join(arg, " ")) + return exec.CommandContext(ctx, name, arg...) +}