• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

umputun / unfuck-ai-comments / 13581507943

28 Feb 2025 04:44AM UTC coverage: 72.404% (+0.08%) from 72.321%
13581507943

Pull #3

github

umputun
Fix inconsistent struct field alignment with gofmt

Add -s flag to gofmt commands to preserve existing alignment style when processing files with --fmt option.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Pull Request #3: Fix inconsistent struct field alignment with gofmt

244 of 337 relevant lines covered (72.4%)

28851.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.4
/main.go
1
package main
2

3
import (
4
        "errors"
5
        "fmt"
6
        "go/ast"
7
        "go/parser"
8
        "go/printer"
9
        "go/token"
10
        "os"
11
        "os/exec"
12
        "path/filepath"
13
        "strings"
14
        "unicode"
15

16
        "github.com/fatih/color"
17
        "github.com/jessevdk/go-flags"
18
)
19

20
// Options holds command line options
21
type Options struct {
22
        Run        struct {
23
                Args struct {
24
                        Patterns []string `positional-arg-name:"FILE/PATTERN" description:"Files or patterns to process (default: current directory)"`
25
                } `positional-args:"yes"`
26
        }        `command:"run" description:"Process files in-place (default)"`
27

28
        Diff        struct {
29
                Args struct {
30
                        Patterns []string `positional-arg-name:"FILE/PATTERN" description:"Files or patterns to process (default: current directory)"`
31
                } `positional-args:"yes"`
32
        }        `command:"diff" description:"Show diff without modifying files"`
33

34
        Print        struct {
35
                Args struct {
36
                        Patterns []string `positional-arg-name:"FILE/PATTERN" description:"Files or patterns to process (default: current directory)"`
37
                } `positional-args:"yes"`
38
        }        `command:"print" description:"Print processed content to stdout"`
39

40
        Title        bool                `long:"title" description:"Convert only the first character to lowercase, keep the rest unchanged"`
41
        Skip        []string        `long:"skip" description:"Skip specified directories or files (can be used multiple times)"`
42
        Format        bool                `long:"fmt" description:"Run gofmt on processed files"`
43

44
        DryRun        bool        `long:"dry" description:"Don't modify files, just show what would be changed"`
45
}
46

47
var osExit = os.Exit        // replace os.Exit with a variable for testing
48

49
func main() {
×
50
        // define options
×
51
        var opts Options
×
52
        p := flags.NewParser(&opts, flags.Default)
×
53

×
54
        p.LongDescription = "Convert in-function comments to lowercase while preserving comments outside functions"
×
55

×
56
        // handle parsing errors
×
57
        if _, err := p.Parse(); err != nil {
×
58
                var flagsErr *flags.Error
×
59
                if errors.As(err, &flagsErr) && errors.Is(flagsErr.Type, flags.ErrHelp) {
×
60
                        osExit(0)
×
61
                }
×
62
                fmt.Fprintf(os.Stderr, "Error: %s\n", err)
×
63
                osExit(1)
×
64
        }
65

66
        // determine the mode based on command or flags
67
        mode := "inplace"        // default
×
68

×
69
        var args []string
×
70
        // process according to command or flags
×
71
        if p.Command.Active != nil {
×
72
                // command was specified
×
73
                switch p.Command.Active.Name {
×
74
                case "run":
×
75
                        mode = "inplace"
×
76
                        args = opts.Run.Args.Patterns
×
77
                case "diff":
×
78
                        mode = "diff"
×
79
                        args = opts.Diff.Args.Patterns
×
80
                case "print":
×
81
                        mode = "print"
×
82
                        args = opts.Print.Args.Patterns
×
83
                }
84
        }
85

86
        if opts.DryRun {
×
87
                mode = "diff"
×
88
                args = opts.Run.Args.Patterns
×
89
        }
×
90

91
        // create process request with all options
92
        req := ProcessRequest{
×
93
                OutputMode:        mode,
×
94
                TitleCase:        opts.Title,
×
95
                Format:                opts.Format,
×
96
                SkipPatterns:        opts.Skip,
×
97
        }
×
98

×
99
        // process each pattern
×
100
        for _, pattern := range patterns(args) {
×
101
                processPattern(pattern, req)
×
102
        }
×
103
}
104

105
// patterns to process, defaulting to current directory
106
func patterns(p []string) []string {
×
107
        res := p
×
108
        if len(res) == 0 {
×
109
                res = []string{"."}
×
110
        }
×
111
        return res
×
112
}
113

114
// ProcessRequest contains all processing parameters
115
type ProcessRequest struct {
116
        OutputMode        string
117
        TitleCase        bool
118
        Format                bool
119
        SkipPatterns        []string
120
}
121

122
// processPattern processes a single pattern
123
func processPattern(pattern string, req ProcessRequest) {
12✔
124
        // handle special "./..." pattern for recursive search
12✔
125
        if pattern == "./..." {
14✔
126
                walkDir(".", req)
2✔
127
                return
2✔
128
        }
2✔
129

130
        // if it's a recursive pattern, handle it
131
        if strings.HasSuffix(pattern, "/...") || strings.HasSuffix(pattern, "...") {
11✔
132
                // extract the directory part
1✔
133
                dir := strings.TrimSuffix(pattern, "/...")
1✔
134
                dir = strings.TrimSuffix(dir, "...")
1✔
135
                if dir == "" {
1✔
136
                        dir = "."
×
137
                }
×
138
                walkDir(dir, req)
1✔
139
                return
1✔
140
        }
141

142
        // initialize files slice
143
        var files []string
9✔
144

9✔
145
        // first check if the pattern is a directory
9✔
146
        fileInfo, err := os.Stat(pattern)
9✔
147
        if err == nil && fileInfo.IsDir() {
12✔
148
                // it's a directory, find go files in it
3✔
149
                globPattern := filepath.Join(pattern, "*.go")
3✔
150
                matches, err := filepath.Glob(globPattern)
3✔
151
                if err != nil {
3✔
152
                        fmt.Fprintf(os.Stderr, "Error finding Go files in %s: %v\n", pattern, err)
×
153
                }
×
154
                if len(matches) > 0 {
6✔
155
                        files = matches
3✔
156
                }
3✔
157
        } else {
6✔
158
                // not a directory, try as a glob pattern
6✔
159
                files, err = filepath.Glob(pattern)
6✔
160
                if err != nil {
6✔
161
                        fmt.Fprintf(os.Stderr, "Error globbing pattern %s: %v\n", pattern, err)
×
162
                        return
×
163
                }
×
164
        }
165

166
        if len(files) == 0 {
10✔
167
                fmt.Printf("No Go files found matching pattern: %s\n", pattern)
1✔
168
                return
1✔
169
        }
1✔
170

171
        // process each file
172
        for _, file := range files {
18✔
173
                if !strings.HasSuffix(file, ".go") {
10✔
174
                        continue
×
175
                }
176

177
                // check if file should be skipped
178
                if shouldSkip(file, req.SkipPatterns) {
11✔
179
                        continue
1✔
180
                }
181

182
                processFile(file, req.OutputMode, req.TitleCase, req.Format)
9✔
183
        }
184
}
185

186
// walkDir recursively processes all .go files in directory and subdirectories
187
func walkDir(dir string, req ProcessRequest) {
3✔
188
        err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
15✔
189
                if err != nil {
12✔
190
                        return err
×
191
                }
×
192

193
                // skip vendor directories
194
                if info.IsDir() && (info.Name() == "vendor" || strings.Contains(path, "/vendor/")) {
12✔
195
                        return filepath.SkipDir
×
196
                }
×
197

198
                // check if directory should be skipped
199
                if info.IsDir() && shouldSkip(path, req.SkipPatterns) {
13✔
200
                        return filepath.SkipDir
1✔
201
                }
1✔
202

203
                if !info.IsDir() && strings.HasSuffix(path, ".go") {
17✔
204
                        // check if file should be skipped
6✔
205
                        if shouldSkip(path, req.SkipPatterns) {
6✔
206
                                return nil
×
207
                        }
×
208
                        processFile(path, req.OutputMode, req.TitleCase, req.Format)
6✔
209
                }
210
                return nil
11✔
211
        })
212
        if err != nil {
3✔
213
                fmt.Fprintf(os.Stderr, "Error walking directory %s: %v\n", dir, err)
×
214
        }
×
215
}
216

217
// shouldSkip checks if a path should be skipped based on skip patterns
218
func shouldSkip(path string, skipPatterns []string) bool {
29✔
219
        if len(skipPatterns) == 0 {
44✔
220
                return false
15✔
221
        }
15✔
222

223
        // normalize path
224
        normalizedPath := filepath.Clean(path)
14✔
225

14✔
226
        for _, skipPattern := range skipPatterns {
30✔
227
                // check for exact match
16✔
228
                if skipPattern == normalizedPath {
19✔
229
                        return true
3✔
230
                }
3✔
231

232
                // check if path is within a skipped directory
233
                if strings.HasPrefix(normalizedPath, skipPattern+string(filepath.Separator)) {
14✔
234
                        return true
1✔
235
                }
1✔
236

237
                // check for glob pattern match
238
                matched, err := filepath.Match(skipPattern, normalizedPath)
12✔
239
                if err == nil && matched {
12✔
240
                        return true
×
241
                }
×
242

243
                // also check just the base name for simple pattern matching
244
                matched, err = filepath.Match(skipPattern, filepath.Base(normalizedPath))
12✔
245
                if err == nil && matched {
14✔
246
                        return true
2✔
247
                }
2✔
248
        }
249

250
        return false
8✔
251
}
252

253
// runGoFmt runs gofmt on the specified file
254
func runGoFmt(fileName string) {
4✔
255
        // use gofmt with settings that preserve original formatting as much as possible
4✔
256
        cmd := exec.Command("gofmt", "-w", "-s", fileName)
4✔
257
        output, err := cmd.CombinedOutput()
4✔
258
        if err != nil {
4✔
259
                fmt.Fprintf(os.Stderr, "Error running gofmt on %s: %v\n%s", fileName, err, output)
×
260
        }
×
261
}
262

263
// formatWithGofmt formats the given content with gofmt
264
// returns the original content if formatting fails
265
func formatWithGofmt(content string) string {
3✔
266
        cmd := exec.Command("gofmt", "-s")
3✔
267
        cmd.Stdin = strings.NewReader(content)
3✔
268

3✔
269
        // capture the stdout output
3✔
270
        formattedBytes, err := cmd.Output()
3✔
271
        if err != nil {
3✔
272
                fmt.Fprintf(os.Stderr, "Error formatting with gofmt: %v\n", err)
×
273
                return content        // return original content on error
×
274
        }
×
275

276
        return string(formattedBytes)
3✔
277
}
278

279
func processFile(fileName, outputMode string, titleCase, format bool) {
26✔
280
        // parse the file
26✔
281
        fset := token.NewFileSet()
26✔
282
        node, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments)
26✔
283
        if err != nil {
27✔
284
                fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", fileName, err)
1✔
285
                return
1✔
286
        }
1✔
287

288
        // process comments
289
        modified := false
25✔
290
        for _, commentGroup := range node.Comments {
307✔
291
                for _, comment := range commentGroup.List {
572✔
292
                        // check if comment is inside a function
290✔
293
                        if isCommentInsideFunction(fset, node, comment) {
550✔
294
                                // process the comment text
260✔
295
                                orig := comment.Text
260✔
296
                                var processed string
260✔
297
                                if titleCase {
260✔
298
                                        processed = convertCommentToTitleCase(orig)
×
299
                                } else {
260✔
300
                                        processed = convertCommentToLowercase(orig)
260✔
301
                                }
260✔
302
                                if orig != processed {
289✔
303
                                        comment.Text = processed
29✔
304
                                        modified = true
29✔
305
                                }
29✔
306
                        }
307
                }
308
        }
309

310
        // if no comments were modified, no need to proceed
311
        if !modified {
27✔
312
                return
2✔
313
        }
2✔
314

315
        // handle output based on specified mode
316
        switch outputMode {
23✔
317
        case "inplace":
16✔
318
                // write modified source back to file
16✔
319
                file, err := os.Create(fileName)        //nolint:gosec
16✔
320
                if err != nil {
16✔
321
                        fmt.Fprintf(os.Stderr, "Error opening %s for writing: %v\n", fileName, err)
×
322
                        return
×
323
                }
×
324
                defer file.Close()
16✔
325
                if err := printer.Fprint(file, fset, node); err != nil {
16✔
326
                        fmt.Fprintf(os.Stderr, "Error writing to file %s: %v\n", fileName, err)
×
327
                        return
×
328
                }
×
329
                fmt.Printf("Updated: %s\n", fileName)
16✔
330

16✔
331
                // run gofmt if requested
16✔
332
                if format {
20✔
333
                        runGoFmt(fileName)
4✔
334
                }
4✔
335

336
        case "print":
2✔
337
                // print modified source to stdout
2✔
338
                var modifiedBytes strings.Builder
2✔
339
                if err := printer.Fprint(&modifiedBytes, fset, node); err != nil {
2✔
340
                        fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
×
341
                        return
×
342
                }
×
343

344
                // if format is requested, use our helper function
345
                if format {
3✔
346
                        formattedContent := formatWithGofmt(modifiedBytes.String())
1✔
347
                        fmt.Print(formattedContent)
1✔
348
                } else {
2✔
349
                        fmt.Print(modifiedBytes.String())
1✔
350
                }
1✔
351

352
        case "diff":
5✔
353
                // generate diff output
5✔
354
                origBytes, err := os.ReadFile(fileName)        //nolint:gosec
5✔
355
                if err != nil {
5✔
356
                        fmt.Fprintf(os.Stderr, "Error reading original file %s: %v\n", fileName, err)
×
357
                        return
×
358
                }
×
359

360
                // generate modified content
361
                var modifiedBytes strings.Builder
5✔
362
                if err := printer.Fprint(&modifiedBytes, fset, node); err != nil {
5✔
363
                        fmt.Fprintf(os.Stderr, "Error creating diff: %v\n", err)
×
364
                        return
×
365
                }
×
366

367
                // get original and modified content
368
                originalContent := string(origBytes)
5✔
369
                modifiedContent := modifiedBytes.String()
5✔
370

5✔
371
                // apply formatting if requested
5✔
372
                if format {
6✔
373
                        // format both original and modified content for consistency
1✔
374
                        originalContent = formatWithGofmt(originalContent)
1✔
375
                        modifiedContent = formatWithGofmt(modifiedContent)
1✔
376
                }
1✔
377

378
                // use cyan for file information
379
                cyan := color.New(color.FgCyan, color.Bold).SprintFunc()
5✔
380
                fmt.Printf("%s\n", cyan("--- "+fileName+" (original)"))
5✔
381
                fmt.Printf("%s\n", cyan("+++ "+fileName+" (modified)"))
5✔
382

5✔
383
                // print the diff with colors
5✔
384
                fmt.Print(simpleDiff(originalContent, modifiedContent))
5✔
385
        }
386
}
387

388
// isCommentInsideFunction checks if a comment is inside a function declaration
389
func isCommentInsideFunction(_ *token.FileSet, file *ast.File, comment *ast.Comment) bool {
310✔
390
        commentPos := comment.Pos()
310✔
391

310✔
392
        // find function containing the comment
310✔
393
        var insideFunc bool
310✔
394
        ast.Inspect(file, func(n ast.Node) bool {
1,940,539✔
395
                if n == nil {
2,910,208✔
396
                        return true
969,979✔
397
                }
969,979✔
398

399
                // check if this is a function declaration
400
                fn, isFunc := n.(*ast.FuncDecl)
970,250✔
401
                if isFunc {
974,046✔
402
                        // check if comment is inside function body
3,796✔
403
                        if fn.Body != nil && fn.Body.Lbrace <= commentPos && commentPos <= fn.Body.Rbrace {
4,067✔
404
                                insideFunc = true
271✔
405
                                return false        // stop traversal
271✔
406
                        }
271✔
407
                }
408
                return true
969,979✔
409
        })
410

411
        return insideFunc
310✔
412
}
413

414
// convertCommentToLowercase converts a comment to lowercase, preserving the comment markers
415
func convertCommentToLowercase(comment string) string {
269✔
416
        if strings.HasPrefix(comment, "//") {
535✔
417
                // single line comment
266✔
418
                content := strings.TrimPrefix(comment, "//")
266✔
419
                return "//" + strings.ToLower(content)
266✔
420
        }
266✔
421
        if strings.HasPrefix(comment, "/*") && strings.HasSuffix(comment, "*/") {
5✔
422
                // multi-line comment
2✔
423
                content := strings.TrimSuffix(strings.TrimPrefix(comment, "/*"), "*/")
2✔
424
                return "/*" + strings.ToLower(content) + "*/"
2✔
425
        }
2✔
426
        return comment
1✔
427
}
428

429
// convertCommentToTitleCase converts only the first character of a comment to lowercase,
430
// preserving the case of the rest of the text and the comment markers
431
func convertCommentToTitleCase(comment string) string {
8✔
432
        if strings.HasPrefix(comment, "//") {
14✔
433
                // single line comment
6✔
434
                content := strings.TrimPrefix(comment, "//")
6✔
435
                if content != "" {
11✔
436
                        // skip leading whitespace
5✔
437
                        i := 0
5✔
438
                        for i < len(content) && unicode.IsSpace(rune(content[i])) {
11✔
439
                                i++
6✔
440
                        }
6✔
441

442
                        // if there's content after whitespace
443
                        if i < len(content) {
10✔
444
                                // convert only first non-whitespace character to lowercase
5✔
445
                                prefix := content[:i]
5✔
446
                                firstChar := strings.ToLower(string(content[i]))
5✔
447
                                restOfContent := content[i+1:]
5✔
448
                                return "//" + prefix + firstChar + restOfContent
5✔
449
                        }
5✔
450
                }
451
                return "//" + content
1✔
452
        }
453
        if strings.HasPrefix(comment, "/*") && strings.HasSuffix(comment, "*/") {
4✔
454
                // multi-line comment
2✔
455
                content := strings.TrimSuffix(strings.TrimPrefix(comment, "/*"), "*/")
2✔
456

2✔
457
                // split by lines to handle multi-line comments
2✔
458
                lines := strings.Split(content, "\n")
2✔
459
                if len(lines) > 0 {
4✔
460
                        // process the first line
2✔
461
                        line := lines[0]
2✔
462
                        i := 0
2✔
463
                        for i < len(line) && unicode.IsSpace(rune(line[i])) {
3✔
464
                                i++
1✔
465
                        }
1✔
466

467
                        if i < len(line) {
3✔
468
                                prefix := line[:i]
1✔
469
                                firstChar := strings.ToLower(string(line[i]))
1✔
470
                                restOfLine := line[i+1:]
1✔
471
                                lines[0] = prefix + firstChar + restOfLine
1✔
472
                        }
1✔
473

474
                        return "/*" + strings.Join(lines, "\n") + "*/"
2✔
475
                }
476

477
                return "/*" + content + "*/"
×
478
        }
479
        return comment
×
480
}
481

482
// simpleDiff creates a colorized diff output
483
func simpleDiff(original, modified string) string {
10✔
484
        origLines := strings.Split(original, "\n")
10✔
485
        modLines := strings.Split(modified, "\n")
10✔
486

10✔
487
        // set up colors - use bright versions for better visibility
10✔
488
        red := color.New(color.FgRed, color.Bold).SprintFunc()
10✔
489
        green := color.New(color.FgGreen, color.Bold).SprintFunc()
10✔
490

10✔
491
        var diff strings.Builder
10✔
492

10✔
493
        for i := 0; i < len(origLines) || i < len(modLines); i++ {
54✔
494
                switch {
44✔
495
                case i >= len(origLines):
8✔
496
                        diff.WriteString(green("+ "+modLines[i]) + "\n")
8✔
497
                case i >= len(modLines):
1✔
498
                        diff.WriteString(red("- "+origLines[i]) + "\n")
1✔
499
                case origLines[i] != modLines[i]:
16✔
500
                        diff.WriteString(red("- "+origLines[i]) + "\n")
16✔
501
                        diff.WriteString(green("+ "+modLines[i]) + "\n")
16✔
502
                }
503
        }
504

505
        return diff.String()
10✔
506
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc