2026-03-09 13:13:30 +00:00
|
|
|
package php
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
2026-03-16 21:29:34 +00:00
|
|
|
|
|
|
|
|
coreerr "forge.lthn.ai/core/go-log"
|
2026-03-09 13:13:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RectorOptions configures Rector code refactoring.
|
|
|
|
|
type RectorOptions struct {
|
|
|
|
|
Dir string
|
|
|
|
|
Fix bool // Apply changes (default is dry-run)
|
|
|
|
|
Diff bool // Show detailed diff
|
|
|
|
|
ClearCache bool // Clear cache before running
|
|
|
|
|
Output io.Writer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DetectRector checks if Rector is available in the project.
|
|
|
|
|
func DetectRector(dir string) bool {
|
|
|
|
|
// Check for rector.php config
|
|
|
|
|
rectorConfig := filepath.Join(dir, "rector.php")
|
|
|
|
|
if fileExists(rectorConfig) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for vendor binary
|
|
|
|
|
rectorBin := filepath.Join(dir, "vendor", "bin", "rector")
|
|
|
|
|
if fileExists(rectorBin) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RunRector runs Rector for automated code refactoring.
|
|
|
|
|
func RunRector(ctx context.Context, opts RectorOptions) error {
|
|
|
|
|
if opts.Dir == "" {
|
|
|
|
|
cwd, err := os.Getwd()
|
|
|
|
|
if err != nil {
|
2026-03-16 21:29:34 +00:00
|
|
|
return coreerr.E("php.RunRector", "get working directory", err)
|
2026-03-09 13:13:30 +00:00
|
|
|
}
|
|
|
|
|
opts.Dir = cwd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Output == nil {
|
|
|
|
|
opts.Output = os.Stdout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build command
|
|
|
|
|
vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "rector")
|
|
|
|
|
cmdName := "rector"
|
|
|
|
|
if fileExists(vendorBin) {
|
|
|
|
|
cmdName = vendorBin
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args := []string{"process"}
|
|
|
|
|
|
|
|
|
|
if !opts.Fix {
|
|
|
|
|
args = append(args, "--dry-run")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Diff {
|
|
|
|
|
args = append(args, "--output-format", "diff")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.ClearCache {
|
|
|
|
|
args = append(args, "--clear-cache")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := exec.CommandContext(ctx, cmdName, args...)
|
|
|
|
|
cmd.Dir = opts.Dir
|
|
|
|
|
cmd.Stdout = opts.Output
|
|
|
|
|
cmd.Stderr = opts.Output
|
|
|
|
|
|
|
|
|
|
return cmd.Run()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildRectorCommand builds the command for running Rector (exported for testing).
|
|
|
|
|
func buildRectorCommand(opts RectorOptions) (string, []string) {
|
|
|
|
|
vendorBin := filepath.Join(opts.Dir, "vendor", "bin", "rector")
|
|
|
|
|
cmdName := "rector"
|
|
|
|
|
if fileExists(vendorBin) {
|
|
|
|
|
cmdName = vendorBin
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args := []string{"process"}
|
|
|
|
|
|
|
|
|
|
if !opts.Fix {
|
|
|
|
|
args = append(args, "--dry-run")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Diff {
|
|
|
|
|
args = append(args, "--output-format", "diff")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.ClearCache {
|
|
|
|
|
args = append(args, "--clear-cache")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmdName, args
|
|
|
|
|
}
|