diff --git a/cmd/extension/extension_fix.go b/cmd/extension/extension_fix.go index 0dec60fd..fb313062 100644 --- a/cmd/extension/extension_fix.go +++ b/cmd/extension/extension_fix.go @@ -1,6 +1,7 @@ package extension import ( + "errors" "fmt" "os" "path/filepath" @@ -18,10 +19,23 @@ var extensionFixCmd = &cobra.Command{ Use: "fix path", Short: "Apply code-quality fixes to an extension", Args: cobra.ExactArgs(1), - PreRunE: func(cmd *cobra.Command, args []string) error { - return verifier.SetupTools(cmd.Context(), cmd.Root().Version) - }, RunE: func(cmd *cobra.Command, args []string) error { + allTools := verifier.GetToolsOf[verifier.FixTool]() + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + requestedTools, err := allTools.Only(only) + if err != nil { + return err + } + tools, err := requestedTools.Exclude(exclude) + if err != nil { + return err + } + if len(tools) == 0 { + return errors.New("no fixers selected after applying --exclude") + } + allowNonGit, _ := cmd.Flags().GetBool("allow-non-git") if !allowNonGit { @@ -40,7 +54,7 @@ var extensionFixCmd = &cobra.Command{ return err } - toolCfg, err := verifier.ConvertExtensionToToolConfig(ext) + toolCfg, err := verifier.SetupExtensionToolConfig(cmd.Context(), cmd.Root().Version, ext) if err != nil { return err } @@ -49,14 +63,6 @@ var extensionFixCmd = &cobra.Command{ var gr errgroup.Group - allTools := verifier.GetToolsOf[verifier.FixTool]() - only, _ := cmd.Flags().GetString("only") - - tools, err := allTools.Only(only) - if err != nil { - return err - } - for _, tool := range tools { gr.Go(func() error { return tool.Fix(cmd.Context(), *toolCfg) @@ -64,7 +70,7 @@ var extensionFixCmd = &cobra.Command{ } runErr := gr.Wait() - if err := validation.PrintToolInvocationTable(os.Stdout, "Fixers", extensionToolInvocationStatuses(allTools, tools)); err != nil { + if err := validation.PrintToolInvocationTable(os.Stdout, "Fixers", extensionToolInvocationStatuses(allTools, requestedTools, tools)); err != nil { return err } return runErr @@ -74,5 +80,6 @@ var extensionFixCmd = &cobra.Command{ func init() { extensionRootCmd.AddCommand(extensionFixCmd) extensionFixCmd.Flags().String("only", "", "Run only specific fixers by name (comma-separated, e.g. eslint,rector)") + extensionFixCmd.Flags().String("exclude", "", "Exclude fixers after applying --only (comma-separated, e.g. eslint,rector)") extensionFixCmd.Flags().Bool("allow-non-git", false, "Allow running the fix command on non-git repositories") } diff --git a/cmd/extension/extension_format.go b/cmd/extension/extension_format.go index eb437666..50b3cfce 100644 --- a/cmd/extension/extension_format.go +++ b/cmd/extension/extension_format.go @@ -1,6 +1,7 @@ package extension import ( + "errors" "fmt" "os" "path/filepath" @@ -18,10 +19,23 @@ var extensionFormat = &cobra.Command{ Use: "format path", Short: "Format an extension's PHP, JavaScript, SCSS, and Administration Twig files", Args: cobra.ExactArgs(1), - PreRunE: func(cmd *cobra.Command, args []string) error { - return verifier.SetupTools(cmd.Context(), cmd.Root().Version) - }, RunE: func(cmd *cobra.Command, args []string) error { + allTools := verifier.GetToolsOf[verifier.FormatTool]() + only, _ := cmd.Flags().GetString("only") + exclude, _ := cmd.Flags().GetString("exclude") + + requestedTools, err := allTools.Only(only) + if err != nil { + return err + } + tools, err := requestedTools.Exclude(exclude) + if err != nil { + return err + } + if len(tools) == 0 { + return errors.New("no formatters selected after applying --exclude") + } + dryRun, _ := cmd.Flags().GetBool("dry-run") path, err := filepath.Abs(args[0]) @@ -34,7 +48,7 @@ var extensionFormat = &cobra.Command{ return err } - toolCfg, err := verifier.ConvertExtensionToToolConfig(ext) + toolCfg, err := verifier.SetupExtensionToolConfig(cmd.Context(), cmd.Root().Version, ext) if err != nil { return err } @@ -43,14 +57,6 @@ var extensionFormat = &cobra.Command{ var gr errgroup.Group - allTools := verifier.GetToolsOf[verifier.FormatTool]() - only, _ := cmd.Flags().GetString("only") - - tools, err := allTools.Only(only) - if err != nil { - return err - } - for _, tool := range tools { gr.Go(func() error { return tool.Format(cmd.Context(), *toolCfg, dryRun) @@ -58,7 +64,7 @@ var extensionFormat = &cobra.Command{ } runErr := gr.Wait() - if err := validation.PrintToolInvocationTable(os.Stdout, "Formatters", extensionToolInvocationStatuses(allTools, tools)); err != nil { + if err := validation.PrintToolInvocationTable(os.Stdout, "Formatters", extensionToolInvocationStatuses(allTools, requestedTools, tools)); err != nil { return err } return runErr @@ -68,5 +74,6 @@ var extensionFormat = &cobra.Command{ func init() { extensionRootCmd.AddCommand(extensionFormat) extensionFormat.Flags().String("only", "", "Run only specific formatters by name (comma-separated, e.g. prettier,php-cs-fixer)") + extensionFormat.Flags().String("exclude", "", "Exclude formatters after applying --only (comma-separated, e.g. prettier,php-cs-fixer)") extensionFormat.Flags().Bool("dry-run", false, "Run in dry run mode") } diff --git a/cmd/extension/extension_tool_invocation.go b/cmd/extension/extension_tool_invocation.go index d8def664..dcdcadb4 100644 --- a/cmd/extension/extension_tool_invocation.go +++ b/cmd/extension/extension_tool_invocation.go @@ -8,13 +8,16 @@ import ( "github.com/shopware/shopware-cli/internal/verifier" ) -func extensionToolInvocationStatuses[T verifier.Tool](all, selected verifier.ToolList[T]) []validation.ToolInvocationStatus { +func extensionToolInvocationStatuses[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"} - if slices.ContainsFunc(selected, func(selected T) bool { return selected.Name() == tool.Name() }) { + 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) } diff --git a/cmd/extension/extension_tool_invocation_test.go b/cmd/extension/extension_tool_invocation_test.go index d5b6474b..b724ef76 100644 --- a/cmd/extension/extension_tool_invocation_test.go +++ b/cmd/extension/extension_tool_invocation_test.go @@ -16,12 +16,23 @@ func TestExtensionToolInvocationStatuses(t *testing.T) { func assertExtensionToolInvocationStatuses[T verifier.Tool](t *testing.T, all verifier.ToolList[T]) { t.Helper() - selected, err := all.Only(all[0].Name()) + requested, err := all.Only(all[0].Name() + "," + all[1].Name()) + require.NoError(t, err) + selected, err := requested.Exclude(all[1].Name()) require.NoError(t, err) - statuses := extensionToolInvocationStatuses(all, selected) + statuses := extensionToolInvocationStatuses(all, requested, selected) assert.Len(t, statuses, len(all)) assert.Equal(t, "invoked", toolStatusByName(t, statuses, all[0].Name()).Status) assert.Equal(t, "skipped", toolStatusByName(t, statuses, all[1].Name()).Status) - assert.Equal(t, "not selected by --only", toolStatusByName(t, statuses, all[1].Name()).Reason) + assert.Equal(t, "excluded by --exclude", toolStatusByName(t, statuses, all[1].Name()).Reason) + assert.Equal(t, "not selected by --only", toolStatusByName(t, statuses, all[2].Name()).Reason) + + _, err = requested.Exclude(all[2].Name()) + require.ErrorContains(t, err, "not found") +} + +func TestExtensionFixAndFormatHaveExcludeFlag(t *testing.T) { + assert.NotNil(t, extensionFixCmd.Flags().Lookup("exclude")) + assert.NotNil(t, extensionFormat.Flags().Lookup("exclude")) } diff --git a/cmd/extension/extension_validate.go b/cmd/extension/extension_validate.go index 348f5db4..9fc4da0c 100644 --- a/cmd/extension/extension_validate.go +++ b/cmd/extension/extension_validate.go @@ -152,14 +152,9 @@ func selectExtensionValidationTools(full bool, only, exclude string) (verifier.T return nil, nil, errors.New("no validation checks selected after applying --exclude") } - statuses := extensionToolInvocationStatuses(validationTools, selected) + statuses := extensionToolInvocationStatuses(validationTools, requestedTools, selected) for i := range statuses { - if statuses[i].Status == "invoked" { - continue - } - if slices.ContainsFunc(requestedTools, func(tool verifier.CheckTool) bool { return tool.Name() == statuses[i].Name }) { - statuses[i].Reason = "excluded by --exclude" - } else if only == "" { + if only == "" && statuses[i].Reason == "not selected by --only" { statuses[i].Reason = "not selected; use --full or --only" } } diff --git a/internal/verifier/extension.go b/internal/verifier/extension.go index d24f7ed7..42825185 100644 --- a/internal/verifier/extension.go +++ b/internal/verifier/extension.go @@ -12,6 +12,13 @@ import ( "github.com/shopware/shopware-cli/internal/validation" ) +func SetupExtensionToolConfig(ctx context.Context, version string, ext extension.Extension) (*ToolConfig, error) { + if err := SetupTools(ctx, version); err != nil { + return nil, err + } + return ConvertExtensionToToolConfig(ext) +} + func ConvertExtensionToToolConfig(ext extension.Extension) (*ToolConfig, error) { var ignores []validation.ToolConfigIgnore diff --git a/internal/verifier/extension_test.go b/internal/verifier/extension_test.go new file mode 100644 index 00000000..76218c4f --- /dev/null +++ b/internal/verifier/extension_test.go @@ -0,0 +1,28 @@ +package verifier + +import ( + "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" +) + +func TestSetupExtensionToolConfigUsesConfiguredToolDirectory(t *testing.T) { + stubShopwareVersions(t) + previous := toolDirectory + t.Cleanup(func() { setToolDirectory(previous) }) + setToolDirectory(filepath.Join(t.TempDir(), "stale")) + + toolDir := filepath.Join(t.TempDir(), "tools") + t.Setenv("SHOPWARE_CLI_TOOLS_DIR", toolDir) + ext, err := extension.GetExtensionByFolder(t.Context(), testhelper.NewPlugin(t, "Example")) + require.NoError(t, err) + + cfg, err := SetupExtensionToolConfig(t.Context(), "test", ext) + require.NoError(t, err) + assert.Equal(t, toolDir, cfg.ToolDirectory) +}