diff --git a/internal/cli/action.go b/internal/cli/action.go index 86504fb..becb516 100644 --- a/internal/cli/action.go +++ b/internal/cli/action.go @@ -46,8 +46,9 @@ func (a *action) Run(c *cli.Context) error { imports.FormatterWithDiff(a.flags.diff), ) - if err := f.Format(c.Args().Slice()...); err != nil { - return fmt.Errorf("imports formatter: failed to format %v: %w", c.Args().Slice(), err) + args := c.Args().Slice() + if err := f.Format(args...); err != nil { + return fmt.Errorf("imports formatter: failed to format %v: %w", args, err) } return nil diff --git a/internal/imports/check.go b/internal/imports/check.go new file mode 100644 index 0000000..8b71762 --- /dev/null +++ b/internal/imports/check.go @@ -0,0 +1,37 @@ +package imports + +import ( + "go/ast" + "regexp" + "strings" +) + +var reGoGenerated = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`) + +// Copy from https://github.com/mvdan/gofumpt +func isGoFile(name string) bool { + // Hidden files are ignored + if strings.HasPrefix(name, ".") { + return false + } + + return strings.HasSuffix(name, ".go") +} + +// Copy from https://github.com/mvdan/gofumpt +func isGoGenerated(file *ast.File) bool { + for _, cg := range file.Comments { + // Ignore if package ... is on top + if cg.Pos() > file.Package { + return false + } + + for _, line := range cg.List { + if reGoGenerated.MatchString(line.Text) { + return true + } + } + } + + return false +} diff --git a/internal/imports/formatter.go b/internal/imports/formatter.go index 0d3b9d8..488e6bd 100644 --- a/internal/imports/formatter.go +++ b/internal/imports/formatter.go @@ -1,5 +1,16 @@ package imports +import ( + "errors" + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" +) + +var ErrEmptyPaths = errors.New("empty paths") + type Formatter struct { isList bool isWrite bool @@ -7,16 +18,70 @@ type Formatter struct { } func NewFormmater(opts ...FormatterOptionFn) *Formatter { - f := &Formatter{} + ft := &Formatter{} for _, opt := range opts { - opt(f) + opt(ft) } - return f + return ft } -// Accept a list of files or directories aka fsNames -func (f *Formatter) Format(fsNames ...string) error { +// Accept a list of files or directories +func (ft *Formatter) Format(paths ...string) error { + if len(paths) == 0 { + return ErrEmptyPaths + } + + // Logic switch case copy from goimports, gofumpt + for _, path := range paths { + switch dir, err := os.Stat(path); { + case err != nil: + return fmt.Errorf("os: failed to stat: [%s] %w", path, err) + case dir.IsDir(): + if err := ft.formatDir(path); err != nil { + return err + } + default: + if err := ft.formatFile(path); err != nil { + return err + } + } + } + + return nil +} + +func (ft *Formatter) formatDir(path string) error { + return nil +} + +func (ft *Formatter) formatFile(path string) error { + // Check go file + if !isGoFile(filepath.Base(path)) { + return nil + } + + pathBytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("os: failed to read file: [%s] %w", path, err) + } + + // Parse ast + fset := token.NewFileSet() + + parserMode := parser.Mode(0) + parserMode |= parser.ParseComments + + pathASTFile, err := parser.ParseFile(fset, path, pathBytes, parserMode) + if err != nil { + return fmt.Errorf("parser: failed to parse file [%s]: %w", path, err) + } + + // Ignore generated file + if isGoGenerated(pathASTFile) { + return nil + } + return nil }