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

bombsimon / wsl / 24284911937

11 Apr 2026 02:50PM UTC coverage: 94.19% (-0.08%) from 94.274%
24284911937

Pull #245

github

web-flow
Merge 2ecc73b03 into 2170ba668
Pull Request #245: feat: add cuddle-max-statements config

104 of 107 new or added lines in 4 files covered. (97.2%)

2 existing lines in 1 file now uncovered.

1378 of 1463 relevant lines covered (94.19%)

321.2 hits per line

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

95.74
/wsl.go
1
package wsl
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "go/ast"
7
        "go/format"
8
        "go/token"
9
        "go/types"
10
        "slices"
11

12
        "golang.org/x/tools/go/analysis"
13
)
14

15
const (
16
        messageMissingWhitespaceAbove = "missing whitespace above this line"
17
        messageMissingWhitespaceBelow = "missing whitespace below this line"
18
        messageRemoveWhitespace       = "unnecessary whitespace"
19
)
20

21
type fixRange struct {
22
        fixRangeStart token.Pos
23
        fixRangeEnd   token.Pos
24
        fix           []byte
25
}
26

27
type issue struct {
28
        message string
29
        // We can report multiple fixes at the same position. This happens e.g. when
30
        // we force error cuddling but the error assignment is already cuddled.
31
        // See `checkError` for examples.
32
        fixRanges []fixRange
33
}
34

35
type WSL struct {
36
        file     *ast.File
37
        fset     *token.FileSet
38
        typeInfo *types.Info
39
        issues   map[token.Pos]issue
40
        config   *Configuration
41
}
42

43
func New(file *ast.File, pass *analysis.Pass, cfg *Configuration) *WSL {
41✔
44
        return &WSL{
41✔
45
                fset:     pass.Fset,
41✔
46
                file:     file,
41✔
47
                typeInfo: pass.TypesInfo,
41✔
48
                issues:   make(map[token.Pos]issue),
41✔
49
                config:   cfg,
41✔
50
        }
41✔
51
}
41✔
52

53
// Run will run analysis on the file and pass passed to the constructor. It's
54
// typically only supposed to be used by [analysis.Analyzer].
55
func (w *WSL) Run() {
41✔
56
        ast.Inspect(w.file, func(n ast.Node) bool {
20,763✔
57
                switch node := n.(type) {
20,722✔
58
                case *ast.FuncDecl:
324✔
59
                        w.checkBlock(node.Body, NewCursor([]ast.Stmt{}))
324✔
60
                case *ast.FuncLit:
45✔
61
                        w.checkBlock(node.Body, NewCursor([]ast.Stmt{}))
45✔
62
                }
63

64
                return true
20,722✔
65
        })
66
}
67

68
func (w *WSL) checkStmt(stmt ast.Stmt, cursor *Cursor) {
1,843✔
69
        //nolint:gocritic // This is not commented out code, it's examples
1,843✔
70
        switch s := stmt.(type) {
1,843✔
71
        // if a {} else if b {} else {}
72
        case *ast.IfStmt:
168✔
73
                w.checkIf(s, cursor, false)
168✔
74
        // for {} / for a; b; c {}
75
        case *ast.ForStmt:
30✔
76
                w.checkFor(s, cursor)
30✔
77
        // for _, _ = range a {}
78
        case *ast.RangeStmt:
28✔
79
                w.checkRange(s, cursor)
28✔
80
        // switch {} // switch a {}
81
        case *ast.SwitchStmt:
61✔
82
                w.checkSwitch(s, cursor)
61✔
83
        // switch a.(type) {}
84
        case *ast.TypeSwitchStmt:
13✔
85
                w.checkTypeSwitch(s, cursor)
13✔
86
        // return a
87
        case *ast.ReturnStmt:
70✔
88
                w.checkReturn(s, cursor)
70✔
89
        // continue / break
90
        case *ast.BranchStmt:
29✔
91
                w.checkBranch(s, cursor)
29✔
92
        // var a
93
        case *ast.DeclStmt:
77✔
94
                w.checkDeclStmt(s, cursor)
77✔
95
        // a := a
96
        case *ast.AssignStmt:
611✔
97
                w.checkAssign(s, cursor)
611✔
98
        // a++ / a--
99
        case *ast.IncDecStmt:
83✔
100
                w.checkIncDec(s, cursor)
83✔
101
        // defer func() {}
102
        case *ast.DeferStmt:
26✔
103
                w.checkDefer(s, cursor)
26✔
104
        // go func() {}
105
        case *ast.GoStmt:
19✔
106
                w.checkGo(s, cursor)
19✔
107
        // e.g. someFn()
108
        case *ast.ExprStmt:
390✔
109
                w.checkExprStmt(s, cursor)
390✔
110
        // case:
111
        case *ast.CaseClause:
171✔
112
                w.checkCaseClause(s, cursor)
171✔
113
        // case:
114
        case *ast.CommClause:
32✔
115
                w.checkCommClause(s, cursor)
32✔
116
        // { }
117
        case *ast.BlockStmt:
×
118
                w.checkBlock(s, cursor)
×
119
        // select { }
120
        case *ast.SelectStmt:
13✔
121
                w.checkSelect(s, cursor)
13✔
122
        // ch <- ...
123
        case *ast.SendStmt:
6✔
124
                w.checkSend(s, cursor)
6✔
125
        // LABEL:
126
        case *ast.LabeledStmt:
15✔
127
                w.checkLabel(s, cursor)
15✔
128
        case *ast.EmptyStmt:
1✔
129
        default:
×
130
        }
131
}
132

133
func (w *WSL) checkBody(body []ast.Stmt) {
912✔
134
        cursor := NewCursor(body)
912✔
135

912✔
136
        for cursor.Next() {
2,740✔
137
                w.checkStmt(cursor.Stmt(), cursor)
1,828✔
138
        }
1,828✔
139
}
140

141
func (w *WSL) checkCuddlingBlock(
142
        stmt ast.Node,
143
        blockList []ast.Stmt,
144
        allowedIdents []*ast.Ident,
145
        cursor *Cursor,
146
) {
199✔
147
        var firstBlockStmt ast.Node
199✔
148
        if len(blockList) > 0 {
351✔
149
                firstBlockStmt = blockList[0]
152✔
150
        }
152✔
151

152
        w.checkCuddlingMaxAllowed(stmt, firstBlockStmt, allowedIdents, cursor, true)
199✔
153
}
154

155
func (w *WSL) checkCuddling(stmt ast.Node, cursor *Cursor, enforceLimit bool) {
180✔
156
        w.checkCuddlingMaxAllowed(stmt, nil, []*ast.Ident{}, cursor, enforceLimit)
180✔
157
}
180✔
158

159
func (w *WSL) checkCuddlingMaxAllowed(
160
        stmt ast.Node,
161
        firstBlockStmt ast.Node,
162
        allowedIdents []*ast.Ident,
163
        cursor *Cursor,
164
        enforceLimit bool,
165
) {
379✔
166
        if _, ok := cursor.Stmt().(*ast.LabeledStmt); ok {
387✔
167
                return
8✔
168
        }
8✔
169

170
        previousNode := cursor.PreviousNode()
371✔
171
        numStmtsAbove := w.numberOfStatementsAbove(cursor)
371✔
172
        previousIdents := w.identsFromNode(previousNode, true)
371✔
173

371✔
174
        // If we don't have any statements above, we only care about potential error
371✔
175
        // cuddling (for if statements) so check that.
371✔
176
        if numStmtsAbove == 0 {
593✔
177
                w.checkError(numStmtsAbove, stmt, previousNode, cursor)
222✔
178
                return
222✔
179
        }
222✔
180

181
        if w.isLockOrUnlock(stmt, previousNode) {
158✔
182
                return
9✔
183
        }
9✔
184

185
        _, currIsDefer := stmt.(*ast.DeferStmt)
140✔
186

140✔
187
        // We're cuddled but not with an assign, declare or defer statement which is
140✔
188
        // never allowed.
140✔
189
        if !isAssignDeclOrIncDec(previousNode) && !currIsDefer {
160✔
190
                w.addErrorInvalidTypeCuddle(cursor.Stmt().Pos(), cursor.checkType)
20✔
191
                return
20✔
192
        }
20✔
193

194
        targetIdents := w.cuddleTargetIdents(stmt, firstBlockStmt, allowedIdents)
120✔
195

120✔
196
        if !identsIntersect(previousIdents, targetIdents) {
153✔
197
                w.addErrorNoIntersection(stmt.Pos(), cursor.checkType)
33✔
198
                return
33✔
199
        }
33✔
200

201
        if !enforceLimit {
90✔
202
                return
3✔
203
        }
3✔
204

205
        allowedCount, stoppedAtNonIntersection := w.countValidCuddledStatements(targetIdents, cursor, w.config.CuddleMaxStatements)
84✔
206
        if numStmtsAbove <= allowedCount {
136✔
207
                return
52✔
208
        }
52✔
209

210
        errorNode := cursor.NthPrevious(allowedCount)
32✔
211
        if errorNode == nil {
32✔
UNCOV
212
                return
×
UNCOV
213
        }
×
214

215
        if stoppedAtNonIntersection {
42✔
216
                w.addErrorVariableNotShared(errorNode.Pos(), cursor.checkType)
10✔
217
        } else {
32✔
218
                w.addErrorTooManyStatements(errorNode.Pos(), cursor.checkType)
22✔
219
        }
22✔
220
}
221

222
// cuddleTargetIdents builds the combined set of identifiers that a cuddled
223
// statement may reference. This respects AllowWholeBlock and AllowFirstInBlock.
224
func (w *WSL) cuddleTargetIdents(
225
        stmt ast.Node,
226
        firstBlockStmt ast.Node,
227
        allowedIdents []*ast.Ident,
228
) []*ast.Ident {
120✔
229
        var idents []*ast.Ident
120✔
230

120✔
231
        if w.config.AllowWholeBlock {
139✔
232
                idents = append(idents, w.identsFromNode(stmt, false)...)
19✔
233
        } else {
120✔
234
                idents = append(idents, w.identsFromNode(stmt, true)...)
101✔
235

101✔
236
                if w.config.AllowFirstInBlock {
202✔
237
                        idents = append(idents, w.identsFromNode(firstBlockStmt, true)...)
101✔
238
                }
101✔
239
        }
240

241
        idents = append(idents, allowedIdents...)
120✔
242

120✔
243
        return idents
120✔
244
}
245

246
// countValidCuddledStatements walks backwards from the cursor and counts how
247
// many consecutive cuddled statements have a valid intersection with
248
// targetIdents. It stops at the first non-intersecting statement or when max is
249
// reached (0 = unlimited). Returns the count and whether the walk stopped
250
// because a non-intersecting statement was found (as opposed to the limit).
251
func (w *WSL) countValidCuddledStatements(
252
        targetIdents []*ast.Ident,
253
        cursor *Cursor,
254
        limit int,
255
) (int, bool) {
84✔
256
        defer cursor.Save()()
84✔
257

84✔
258
        currentStmtStartLine := w.lineFor(cursor.Stmt().Pos())
84✔
259
        count := 0
84✔
260

84✔
261
        for cursor.Previous() {
248✔
262
                prevEndLine := w.lineFor(cursor.Stmt().End())
164✔
263
                if prevEndLine != currentStmtStartLine-1 {
177✔
264
                        break
13✔
265
                }
266

267
                if limit > 0 && count >= limit {
173✔
268
                        break
22✔
269
                }
270

271
                prevNode := cursor.Stmt()
129✔
272
                if !isAssignDeclOrIncDec(prevNode) {
129✔
NEW
273
                        break
×
274
                }
275

276
                prevIdents := w.identsFromNode(prevNode, true)
129✔
277
                if !identsIntersect(prevIdents, targetIdents) {
139✔
278
                        return count, true
10✔
279
                }
10✔
280

281
                count++
119✔
282
                currentStmtStartLine = w.lineFor(cursor.Stmt().Pos())
119✔
283
        }
284

285
        return count, false
74✔
286
}
287

288
func (w *WSL) checkCuddlingWithoutIntersection(stmt ast.Node, cursor *Cursor) {
534✔
289
        if w.numberOfStatementsAbove(cursor) == 0 {
894✔
290
                return
360✔
291
        }
360✔
292

293
        if _, ok := cursor.Stmt().(*ast.LabeledStmt); ok {
177✔
294
                return
3✔
295
        }
3✔
296

297
        previousNode := cursor.PreviousNode()
171✔
298

171✔
299
        currAssign, currIsAssign := stmt.(*ast.AssignStmt)
171✔
300
        previousAssign, prevIsAssign := previousNode.(*ast.AssignStmt)
171✔
301
        _, prevIsDecl := previousNode.(*ast.DeclStmt)
171✔
302
        _, prevIsIncDec := previousNode.(*ast.IncDecStmt)
171✔
303

171✔
304
        // Cuddling without intersection is allowed for assignments and inc/dec
171✔
305
        // statements. If however the check for declarations is disabled, we also
171✔
306
        // allow cuddling with them as well.
171✔
307
        //
171✔
308
        // var x string
171✔
309
        // x := ""
171✔
310
        // y++
171✔
311
        if _, ok := w.config.Checks[CheckDecl]; ok {
341✔
312
                prevIsDecl = false
170✔
313
        }
170✔
314

315
        // If we enable exclusive assign checks we only allow new declarations or
316
        // new assignments together but not mix and match.
317
        //
318
        // When this is enabled we also implicitly disable support to cuddle with
319
        // anything else.
320
        if _, ok := w.config.Checks[CheckAssignExclusive]; ok {
180✔
321
                prevIsDecl = false
9✔
322
                prevIsIncDec = false
9✔
323

9✔
324
                if prevIsAssign && currIsAssign {
16✔
325
                        prevIsAssign = previousAssign.Tok == currAssign.Tok
7✔
326
                }
7✔
327
        }
328

329
        prevIsValidType := previousNode == nil || prevIsAssign || prevIsDecl || prevIsIncDec
171✔
330

171✔
331
        if _, ok := w.config.Checks[CheckAssignExpr]; !ok {
338✔
332
                if _, ok := previousNode.(*ast.ExprStmt); ok && w.hasIntersection(stmt, previousNode) {
168✔
333
                        prevIsValidType = prevIsValidType || ok
1✔
334
                }
1✔
335
        }
336

337
        if prevIsValidType {
324✔
338
                return
153✔
339
        }
153✔
340

341
        if w.isLockOrUnlock(stmt, previousNode) {
19✔
342
                return
1✔
343
        }
1✔
344

345
        w.addErrorInvalidTypeCuddle(stmt.Pos(), cursor.checkType)
17✔
346
}
347

348
func (w *WSL) checkBlock(block *ast.BlockStmt, cursor *Cursor) {
709✔
349
        // Block can be nil for function declarations without a body.
709✔
350
        if block == nil {
709✔
351
                return
×
352
        }
×
353

354
        w.checkBlockLeadingNewline(block)
709✔
355
        w.checkTrailingNewline(block)
709✔
356
        w.checkNewlineAfterBlock(block, cursor)
709✔
357

709✔
358
        w.checkBody(block.List)
709✔
359
}
360

361
func (w *WSL) checkNewlineAfterBlock(block *ast.BlockStmt, cursor *Cursor) {
709✔
362
        if _, ok := w.config.Checks[CheckAfterBlock]; !ok {
1,180✔
363
                return
471✔
364
        }
471✔
365

366
        // For function blocks we don't have any statements in our cursor.
367
        if cursor.Len() == 0 {
346✔
368
                return
108✔
369
        }
108✔
370

371
        defer cursor.Save()()
130✔
372

130✔
373
        // Capture current statement and previous node before moving cursor.
130✔
374
        currentStmt := cursor.Stmt()
130✔
375
        previousNode := cursor.PreviousNode()
130✔
376

130✔
377
        if !cursor.Next() {
169✔
378
                // No more statements after this one so check for comments after.
39✔
379
                // Skip comments that are inside the current statement (e.g., inside an else block).
39✔
380
                if cPos := w.commentOnLineAfterNodePos(block); cPos != token.NoPos && cPos >= currentStmt.End() {
41✔
381
                        insertPos := w.lineStartOf(cPos)
2✔
382
                        w.addError(
2✔
383
                                block.Rbrace,
2✔
384
                                insertPos,
2✔
385
                                insertPos,
2✔
386
                                messageMissingWhitespaceBelow,
2✔
387
                                CheckAfterBlock,
2✔
388
                        )
2✔
389
                }
2✔
390

391
                return
39✔
392
        }
393

394
        // Exception: if err != nil { } followed by defer that references
395
        // a variable assigned above the if block.
396
        if w.isErrNotNilCheck(currentStmt) != nil {
94✔
397
                if deferStmt, ok := cursor.Stmt().(*ast.DeferStmt); ok && previousNode != nil {
5✔
398
                        if w.hasIntersection(previousNode, deferStmt) {
3✔
399
                                return
1✔
400
                        }
1✔
401
                }
402
        }
403

404
        rBraceLine := w.lineFor(block.Rbrace)
90✔
405
        nextContentPos := cursor.Stmt().Pos()
90✔
406
        nextContentLine := w.lineFor(nextContentPos)
90✔
407

90✔
408
        // Find the first comment between rbrace and the next statement.
90✔
409
        for _, cg := range w.file.Comments {
1,461✔
410
                if cg.End() <= block.Rbrace {
2,605✔
411
                        continue
1,234✔
412
                }
413

414
                // Skip comments that are inside the current statement but after this block.
415
                // This handles cases like comments inside an else block when checking the if-body.
416
                if cg.Pos() < currentStmt.End() {
138✔
417
                        continue
1✔
418
                }
419

420
                if w.lineFor(cg.End()) == rBraceLine {
186✔
421
                        continue
50✔
422
                }
423

424
                commentLine := w.lineFor(cg.Pos())
86✔
425
                if commentLine > rBraceLine && commentLine < nextContentLine {
114✔
426
                        nextContentPos = cg.Pos()
28✔
427
                        nextContentLine = commentLine
28✔
428
                }
28✔
429

430
                break
86✔
431
        }
432

433
        if nextContentLine <= rBraceLine+1 {
140✔
434
                insertPos := w.lineStartOf(nextContentPos)
50✔
435
                w.addError(
50✔
436
                        block.Rbrace,
50✔
437
                        insertPos,
50✔
438
                        insertPos,
50✔
439
                        messageMissingWhitespaceBelow,
50✔
440
                        CheckAfterBlock,
50✔
441
                )
50✔
442
        }
50✔
443
}
444

445
func (w *WSL) checkCaseClause(stmt *ast.CaseClause, cursor *Cursor) {
171✔
446
        w.checkCaseLeadingNewline(stmt)
171✔
447

171✔
448
        if w.config.CaseMaxLines != 0 {
279✔
449
                w.checkCaseTrailingNewline(stmt.Body, cursor)
108✔
450
        }
108✔
451

452
        w.checkBody(stmt.Body)
171✔
453
}
454

455
func (w *WSL) checkCommClause(stmt *ast.CommClause, cursor *Cursor) {
32✔
456
        w.checkCommLeadingNewline(stmt)
32✔
457

32✔
458
        if w.config.CaseMaxLines != 0 {
49✔
459
                w.checkCaseTrailingNewline(stmt.Body, cursor)
17✔
460
        }
17✔
461

462
        w.checkBody(stmt.Body)
32✔
463
}
464

465
func (w *WSL) checkAssign(stmt *ast.AssignStmt, cursor *Cursor) {
611✔
466
        defer w.checkAppend(stmt, cursor)
611✔
467

611✔
468
        if _, ok := w.config.Checks[CheckAssign]; !ok {
771✔
469
                return
160✔
470
        }
160✔
471

472
        cursor.SetChecker(CheckAssign)
451✔
473

451✔
474
        w.checkCuddlingWithoutIntersection(stmt, cursor)
451✔
475
}
476

477
func (w *WSL) checkAppend(stmt *ast.AssignStmt, cursor *Cursor) {
611✔
478
        if _, ok := w.config.Checks[CheckAppend]; !ok {
766✔
479
                return
155✔
480
        }
155✔
481

482
        if w.numberOfStatementsAbove(cursor) == 0 {
774✔
483
                return
318✔
484
        }
318✔
485

486
        previousNode := cursor.PreviousNode()
138✔
487

138✔
488
        var appendNode *ast.CallExpr
138✔
489

138✔
490
        for _, expr := range stmt.Rhs {
276✔
491
                e, ok := expr.(*ast.CallExpr)
138✔
492
                if !ok {
251✔
493
                        continue
113✔
494
                }
495

496
                if f, ok := e.Fun.(*ast.Ident); ok && f.Name == "append" {
32✔
497
                        appendNode = e
7✔
498
                        break
7✔
499
                }
500
        }
501

502
        if appendNode == nil {
269✔
503
                return
131✔
504
        }
131✔
505

506
        if !w.hasIntersection(appendNode, previousNode) {
9✔
507
                w.addErrorNoIntersection(stmt.Pos(), CheckAppend)
2✔
508
        }
2✔
509
}
510

511
func (w *WSL) checkBranch(stmt *ast.BranchStmt, cursor *Cursor) {
29✔
512
        if _, ok := w.config.Checks[CheckBranch]; !ok {
34✔
513
                return
5✔
514
        }
5✔
515

516
        cursor.SetChecker(CheckBranch)
24✔
517

24✔
518
        if w.numberOfStatementsAbove(cursor) == 0 {
40✔
519
                return
16✔
520
        }
16✔
521

522
        lastStmtInBlock := cursor.statements[len(cursor.statements)-1]
8✔
523
        firstStmts := cursor.Nth(0)
8✔
524

8✔
525
        if w.lineFor(lastStmtInBlock.End())-w.lineFor(firstStmts.Pos()) < w.config.BranchMaxLines {
11✔
526
                return
3✔
527
        }
3✔
528

529
        w.addErrorTooManyLines(stmt.Pos(), cursor.checkType)
5✔
530
}
531

532
func (w *WSL) checkDeclStmt(stmt *ast.DeclStmt, cursor *Cursor) {
77✔
533
        if _, ok := w.config.Checks[CheckDecl]; !ok {
92✔
534
                return
15✔
535
        }
15✔
536

537
        cursor.SetChecker(CheckDecl)
62✔
538

62✔
539
        if w.numberOfStatementsAbove(cursor) == 0 {
95✔
540
                return
33✔
541
        }
33✔
542

543
        // Try to do smart grouping and if we succeed return, otherwise do
544
        // line-by-line fixing.
545
        if w.maybeGroupDecl(stmt, cursor) {
48✔
546
                return
19✔
547
        }
19✔
548

549
        w.addErrorNeverAllow(stmt.Pos(), cursor.checkType)
10✔
550
}
551

552
func (w *WSL) checkDefer(stmt *ast.DeferStmt, cursor *Cursor) {
26✔
553
        w.maybeCheckExpr(
26✔
554
                stmt,
26✔
555
                cursor,
26✔
556
                func(n ast.Node) (bool, bool) {
47✔
557
                        _, previousIsDefer := n.(*ast.DeferStmt)
21✔
558
                        _, previousIsIf := n.(*ast.IfStmt)
21✔
559

21✔
560
                        // We allow defer as a third node only if we have an if statement
21✔
561
                        // between, e.g.
21✔
562
                        //
21✔
563
                        //         f, err := os.Open(file)
21✔
564
                        //         if err != nil {
21✔
565
                        //             return err
21✔
566
                        //         }
21✔
567
                        // defer f.Close()
21✔
568
                        if previousIsIf && w.numberOfStatementsAbove(cursor) >= 2 {
22✔
569
                                defer cursor.Save()()
1✔
570

1✔
571
                                cursor.Previous()
1✔
572
                                cursor.Previous()
1✔
573

1✔
574
                                if w.hasIntersection(cursor.Stmt(), stmt) {
2✔
575
                                        return true, false
1✔
576
                                }
1✔
577
                        }
578

579
                        // Only check cuddling if previous statement isn't also a defer.
580
                        return true, !previousIsDefer
20✔
581
                },
582
                CheckDefer,
583
        )
584
}
585

586
func (w *WSL) checkError(
587
        stmtsAbove int,
588
        ifStmt ast.Node,
589
        previousNode ast.Node,
590
        cursor *Cursor,
591
) {
223✔
592
        if _, ok := w.config.Checks[CheckErr]; !ok {
223✔
593
                return
×
594
        }
×
595

596
        if stmtsAbove > 0 {
223✔
597
                return
×
598
        }
×
599

600
        if _, ok := cursor.Stmt().(*ast.LabeledStmt); ok {
223✔
601
                return
×
602
        }
×
603

604
        defer cursor.Save()()
223✔
605

223✔
606
        errIdent := w.isErrNotNilCheck(ifStmt)
223✔
607
        if errIdent == nil {
434✔
608
                return
211✔
609
        }
211✔
610

611
        previousIdents := []*ast.Ident{}
12✔
612

12✔
613
        if assign, ok := previousNode.(*ast.AssignStmt); ok {
23✔
614
                for _, lhs := range assign.Lhs {
23✔
615
                        previousIdents = append(previousIdents, w.identsFromNode(lhs, true)...)
12✔
616
                }
12✔
617
        }
618

619
        if decl, ok := previousNode.(*ast.DeclStmt); ok {
13✔
620
                if genDecl, ok := decl.Decl.(*ast.GenDecl); ok {
2✔
621
                        for _, spec := range genDecl.Specs {
2✔
622
                                if vs, ok := spec.(*ast.ValueSpec); ok {
2✔
623
                                        previousIdents = append(previousIdents, vs.Names...)
1✔
624
                                }
1✔
625
                        }
626
                }
627
        }
628

629
        // Ensure that the error checked on this line was assigned or declared in
630
        // the previous statement.
631
        if !identsIntersect([]*ast.Ident{errIdent}, previousIdents) {
14✔
632
                return
2✔
633
        }
2✔
634

635
        cursor.SetChecker(CheckErr)
10✔
636

10✔
637
        previousEndLine := w.lineFor(previousNode.End())
10✔
638

10✔
639
        // Check for comments on the same line as the previous node (extends effective end line).
10✔
640
        for _, cg := range w.file.Comments {
61✔
641
                if cg.Pos() >= ifStmt.Pos() {
57✔
642
                        break
6✔
643
                }
644

645
                if cg.Pos() < previousNode.End() || cg.End() > ifStmt.Pos() {
80✔
646
                        continue
35✔
647
                }
648

649
                // There's a comment between the error variable and the if-statement.
650
                // If it's on a different line, we can't do much about this.
651
                if w.lineFor(cg.End()) != previousEndLine {
12✔
652
                        return
2✔
653
                }
2✔
654

655
                // Comment is on the same line - no need to update since line stays the same.
656
        }
657

658
        ifStmtLine := w.lineFor(ifStmt.Pos())
8✔
659
        file := w.fset.File(ifStmt.Pos())
8✔
660

8✔
661
        // Remove blank lines between previous node and if statement.
8✔
662
        removeStart := file.LineStart(previousEndLine + 1)
8✔
663
        removeEnd := file.LineStart(ifStmtLine)
8✔
664
        w.addErrorRemoveNewline(removeStart, removeEnd, cursor.checkType)
8✔
665

8✔
666
        // If we add the error at the same position but with a different fix
8✔
667
        // range, only the fix range will be updated.
8✔
668
        //
8✔
669
        //   a := 1
8✔
670
        //   err := fn()
8✔
671
        //
8✔
672
        //   if err != nil {}
8✔
673
        //
8✔
674
        // Should become
8✔
675
        //
8✔
676
        //   a := 1
8✔
677
        //
8✔
678
        //   err := fn()
8✔
679
        //   if err != nil {}
8✔
680
        cursor.Previous()
8✔
681

8✔
682
        // Add whitespace above the error assignment if there's a statement above.
8✔
683
        if w.numberOfStatementsAbove(cursor) > 0 {
9✔
684
                insertPos := w.lineStartOf(previousNode.Pos())
1✔
685
                w.addError(removeStart, insertPos, insertPos, messageMissingWhitespaceAbove, cursor.checkType)
1✔
686
        }
1✔
687
}
688

689
func (w *WSL) checkExprStmt(stmt *ast.ExprStmt, cursor *Cursor) {
390✔
690
        w.maybeCheckExpr(
390✔
691
                stmt,
390✔
692
                cursor,
390✔
693
                func(n ast.Node) (bool, bool) {
555✔
694
                        _, ok := n.(*ast.ExprStmt)
165✔
695
                        return false, !ok
165✔
696
                },
165✔
697
                CheckExpr,
698
        )
699
}
700

701
func (w *WSL) checkFor(stmt *ast.ForStmt, cursor *Cursor) {
30✔
702
        w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckFor)
30✔
703
}
30✔
704

705
func (w *WSL) checkGo(stmt *ast.GoStmt, cursor *Cursor) {
19✔
706
        w.maybeCheckExpr(
19✔
707
                stmt,
19✔
708
                cursor,
19✔
709
                // We can cuddle any amount `go` statements so only check cuddling if
19✔
710
                // the previous one isn't a `go` call.
19✔
711
                func(n ast.Node) (bool, bool) {
36✔
712
                        _, ok := n.(*ast.GoStmt)
17✔
713
                        return true, !ok
17✔
714
                },
17✔
715
                CheckGo,
716
        )
717
}
718

719
func (w *WSL) checkIf(stmt *ast.IfStmt, cursor *Cursor, isElse bool) {
179✔
720
        // if
179✔
721
        w.checkBlock(stmt.Body, cursor)
179✔
722

179✔
723
        switch v := stmt.Else.(type) {
179✔
724
        // else-if
725
        case *ast.IfStmt:
11✔
726
                w.checkIf(v, cursor, true)
11✔
727

728
        // else
729
        case *ast.BlockStmt:
16✔
730
                w.checkBlock(v, cursor)
16✔
731
        }
732

733
        if _, ok := w.config.Checks[CheckIf]; !isElse && ok {
294✔
734
                cursor.SetChecker(CheckIf)
115✔
735
                w.checkCuddlingBlock(stmt, stmt.Body.List, []*ast.Ident{}, cursor)
115✔
736
        } else if _, ok := w.config.Checks[CheckErr]; !isElse && ok {
180✔
737
                previousNode := cursor.PreviousNode()
1✔
738

1✔
739
                w.checkError(
1✔
740
                        w.numberOfStatementsAbove(cursor),
1✔
741
                        stmt,
1✔
742
                        previousNode,
1✔
743
                        cursor,
1✔
744
                )
1✔
745
        }
1✔
746
}
747

748
func (w *WSL) checkIncDec(stmt *ast.IncDecStmt, cursor *Cursor) {
83✔
749
        if _, ok := w.config.Checks[CheckIncDec]; !ok {
83✔
750
                return
×
751
        }
×
752

753
        cursor.SetChecker(CheckIncDec)
83✔
754

83✔
755
        w.checkCuddlingWithoutIntersection(stmt, cursor)
83✔
756
}
757

758
func (w *WSL) checkLabel(stmt *ast.LabeledStmt, cursor *Cursor) {
15✔
759
        // We check the statement last because the statement is the same node as the
15✔
760
        // label (it's a labeled statement). This means that we _first_ want to
15✔
761
        // check any violations of cuddling the label (never cuddle label) before we
15✔
762
        // actually check the inner statement.
15✔
763
        //
15✔
764
        // It's a subtle difference, but it makes the diagnostic make more sense.
15✔
765
        // We do this by deferring the statmenet check so it happens last no matter
15✔
766
        // if we have label checking enabled or not.
15✔
767
        defer w.checkStmt(stmt.Stmt, cursor)
15✔
768

15✔
769
        if _, ok := w.config.Checks[CheckLabel]; !ok {
17✔
770
                return
2✔
771
        }
2✔
772

773
        cursor.SetChecker(CheckLabel)
13✔
774

13✔
775
        if w.numberOfStatementsAbove(cursor) == 0 {
21✔
776
                return
8✔
777
        }
8✔
778

779
        w.addErrorNeverAllow(stmt.Pos(), cursor.checkType)
5✔
780
}
781

782
func (w *WSL) checkRange(stmt *ast.RangeStmt, cursor *Cursor) {
28✔
783
        w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckRange)
28✔
784
}
28✔
785

786
func (w *WSL) checkReturn(stmt *ast.ReturnStmt, cursor *Cursor) {
70✔
787
        if _, ok := w.config.Checks[CheckReturn]; !ok {
75✔
788
                return
5✔
789
        }
5✔
790

791
        cursor.SetChecker(CheckReturn)
65✔
792

65✔
793
        // There's only a return statement.
65✔
794
        if cursor.Len() <= 1 {
107✔
795
                return
42✔
796
        }
42✔
797

798
        if w.numberOfStatementsAbove(cursor) == 0 {
31✔
799
                return
8✔
800
        }
8✔
801

802
        // If the distance between the first statement and the return statement is
803
        // less than `n` LOC we're allowed to cuddle.
804
        firstStmts := cursor.Nth(0)
15✔
805
        if w.lineFor(stmt.End())-w.lineFor(firstStmts.Pos()) < w.config.BranchMaxLines {
18✔
806
                return
3✔
807
        }
3✔
808

809
        w.addErrorTooManyLines(stmt.Pos(), cursor.checkType)
12✔
810
}
811

812
func (w *WSL) checkSelect(stmt *ast.SelectStmt, cursor *Cursor) {
13✔
813
        w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckSelect)
13✔
814
}
13✔
815

816
func (w *WSL) checkSend(stmt *ast.SendStmt, cursor *Cursor) {
6✔
817
        if _, ok := w.config.Checks[CheckSend]; !ok {
7✔
818
                return
1✔
819
        }
1✔
820

821
        cursor.SetChecker(CheckSend)
5✔
822

5✔
823
        var stmts []ast.Stmt
5✔
824

5✔
825
        ast.Inspect(stmt.Value, func(n ast.Node) bool {
45✔
826
                if b, ok := n.(*ast.BlockStmt); ok {
42✔
827
                        stmts = b.List
2✔
828
                        return false
2✔
829
                }
2✔
830

831
                return true
38✔
832
        })
833

834
        w.checkCuddlingBlock(stmt, stmts, []*ast.Ident{}, cursor)
5✔
835
}
836

837
func (w *WSL) checkSwitch(stmt *ast.SwitchStmt, cursor *Cursor) {
61✔
838
        w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckSwitch)
61✔
839
}
61✔
840

841
func (w *WSL) checkTypeSwitch(stmt *ast.TypeSwitchStmt, cursor *Cursor) {
13✔
842
        w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckTypeSwitch)
13✔
843
}
13✔
844

845
func (w *WSL) checkCaseTrailingNewline(body []ast.Stmt, cursor *Cursor) {
125✔
846
        if len(body) == 0 {
127✔
847
                return
2✔
848
        }
2✔
849

850
        defer cursor.Save()()
123✔
851

123✔
852
        if !cursor.Next() {
174✔
853
                return
51✔
854
        }
51✔
855

856
        var nextCase ast.Node
72✔
857

72✔
858
        switch n := cursor.Stmt().(type) {
72✔
859
        case *ast.CaseClause:
62✔
860
                nextCase = n
62✔
861
        case *ast.CommClause:
10✔
862
                nextCase = n
10✔
863
        default:
×
864
                return
×
865
        }
866

867
        var (
72✔
868
                firstStmt  = body[0]
72✔
869
                lastStmt   = body[len(body)-1]
72✔
870
                totalLines = w.lineFor(nextCase.Pos()) - w.lineFor(firstStmt.Pos())
72✔
871
        )
72✔
872

72✔
873
        if totalLines < w.config.CaseMaxLines {
75✔
874
                return
3✔
875
        }
3✔
876

877
        var (
69✔
878
                lastStmtEndLine = w.lineFor(lastStmt.End())
69✔
879
                nextCaseLine    = w.lineFor(nextCase.Pos())
69✔
880
                nextCaseCol     = w.fset.PositionFor(nextCase.Pos(), false).Column
69✔
881
        )
69✔
882

69✔
883
        // Find transition point between trailing content (indented) and leading
69✔
884
        // content (left-aligned). Trailing comments belong to current case, leading
69✔
885
        // comments belong to next case. The blank line goes at the transition.
69✔
886
        var (
69✔
887
                lastStmtOrCommentEnd         = lastStmt.End()
69✔
888
                nextCaseOrLeftAlignedComment = nextCase.Pos()
69✔
889
                lastLeftAlignedCommentEnd    = token.NoPos
69✔
890
        )
69✔
891

69✔
892
        for _, commentGroup := range w.file.Comments {
1,491✔
893
                if commentGroup.Pos() >= nextCase.Pos() {
1,487✔
894
                        break
65✔
895
                }
896

897
                if commentGroup.End() <= lastStmt.End() {
2,653✔
898
                        continue
1,296✔
899
                }
900

901
                for _, comment := range commentGroup.List {
130✔
902
                        commentLine := w.lineFor(comment.Pos())
69✔
903
                        if commentLine <= lastStmtEndLine || commentLine >= nextCaseLine {
110✔
904
                                continue
41✔
905
                        }
906

907
                        col := w.fset.PositionFor(comment.Pos(), false).Column
28✔
908
                        if col <= nextCaseCol {
42✔
909
                                // Left-aligned: first one marks transition point
14✔
910
                                if lastLeftAlignedCommentEnd == token.NoPos {
24✔
911
                                        nextCaseOrLeftAlignedComment = comment.Pos()
10✔
912
                                }
10✔
913

914
                                lastLeftAlignedCommentEnd = comment.End()
14✔
915
                        } else {
14✔
916
                                // Indented: extend trailing content
14✔
917
                                lastStmtOrCommentEnd = comment.End()
14✔
918
                        }
14✔
919
                }
920
        }
921

922
        lastStmtOrCommentLine := w.lineFor(lastStmtOrCommentEnd)
69✔
923
        nextCaseOrLeadingCommentLine := w.lineFor(nextCaseOrLeftAlignedComment)
69✔
924

69✔
925
        // Check for unnecessary blank line before case (leading comments should be flush).
69✔
926
        if lastLeftAlignedCommentEnd != token.NoPos {
79✔
927
                lastLeadingEndLine := w.lineFor(lastLeftAlignedCommentEnd)
10✔
928

10✔
929
                if lastLeadingEndLine < nextCaseLine-1 {
12✔
930
                        file := w.fset.File(nextCase.Pos())
2✔
931
                        w.addErrorRemoveNewline(file.LineStart(lastLeadingEndLine+1), file.LineStart(nextCaseLine), CheckCaseTrailingNewline)
2✔
932
                }
2✔
933
        }
934

935
        // Already has a blank line at the boundary.
936
        if nextCaseOrLeadingCommentLine > lastStmtOrCommentLine+1 {
98✔
937
                return
29✔
938
        }
29✔
939

940
        insertPos := w.lineStartOf(nextCaseOrLeftAlignedComment)
40✔
941
        w.addError(lastStmtOrCommentEnd, insertPos, insertPos, messageMissingWhitespaceBelow, CheckCaseTrailingNewline)
40✔
942
}
943

944
func (w *WSL) checkBlockLeadingNewline(body *ast.BlockStmt) {
709✔
945
        w.checkLeadingNewline(body.Lbrace, body.List)
709✔
946
}
709✔
947

948
func (w *WSL) checkCaseLeadingNewline(caseClause *ast.CaseClause) {
171✔
949
        w.checkLeadingNewline(caseClause.Colon, caseClause.Body)
171✔
950
}
171✔
951

952
func (w *WSL) checkCommLeadingNewline(commClause *ast.CommClause) {
32✔
953
        w.checkLeadingNewline(commClause.Colon, commClause.Body)
32✔
954
}
32✔
955

956
func (w *WSL) checkLeadingNewline(startPos token.Pos, body []ast.Stmt) {
912✔
957
        if _, ok := w.config.Checks[CheckLeadingWhitespace]; !ok {
1,267✔
958
                return
355✔
959
        }
355✔
960

961
        if len(body) == 0 {
585✔
962
                return
28✔
963
        }
28✔
964

965
        var (
529✔
966
                openLine        = w.lineFor(startPos)
529✔
967
                firstStmtPos    = body[0].Pos()
529✔
968
                firstStmtLine   = w.lineFor(firstStmtPos)
529✔
969
                leadingComments []*ast.CommentGroup
529✔
970
        )
529✔
971

529✔
972
        for _, cg := range w.file.Comments {
4,553✔
973
                if cg.Pos() >= firstStmtPos {
4,512✔
974
                        break
488✔
975
                }
976

977
                if cg.Pos() > startPos {
3,636✔
978
                        leadingComments = append(leadingComments, cg)
100✔
979
                }
100✔
980
        }
981

982
        if len(leadingComments) == 0 {
964✔
983
                if firstStmtLine := w.lineFor(firstStmtPos); firstStmtLine > openLine+1 {
435✔
984
                        file := w.fset.File(startPos)
×
985
                        w.addErrorRemoveNewline(
×
986
                                file.LineStart(openLine+1),
×
987
                                file.LineStart(firstStmtLine),
×
988
                                CheckLeadingWhitespace,
×
989
                        )
×
990
                }
×
991

992
                return
435✔
993
        }
994

995
        var (
94✔
996
                firstContentLine   = firstStmtLine
94✔
997
                lastCommentEndLine = openLine
94✔
998
        )
94✔
999

94✔
1000
        for _, comment := range leadingComments {
194✔
1001
                startLine := w.lineFor(comment.Pos())
100✔
1002
                endLine := w.lineFor(comment.End())
100✔
1003

100✔
1004
                if startLine > openLine && startLine < firstContentLine {
125✔
1005
                        firstContentLine = startLine
25✔
1006
                }
25✔
1007

1008
                if endLine > lastCommentEndLine {
128✔
1009
                        lastCommentEndLine = endLine
28✔
1010
                }
28✔
1011
        }
1012

1013
        file := w.fset.File(startPos)
94✔
1014

94✔
1015
        // Empty line after opening brace.
94✔
1016
        if firstContentLine > openLine+1 {
125✔
1017
                w.addErrorRemoveNewline(
31✔
1018
                        file.LineStart(openLine+1),
31✔
1019
                        file.LineStart(firstContentLine),
31✔
1020
                        CheckLeadingWhitespace,
31✔
1021
                )
31✔
1022
        }
31✔
1023

1024
        // Empty line between comments and first statement.
1025
        if lastCommentEndLine > openLine && firstStmtLine > lastCommentEndLine+1 {
98✔
1026
                w.addErrorRemoveNewline(
4✔
1027
                        file.LineStart(lastCommentEndLine+1),
4✔
1028
                        file.LineStart(firstStmtLine),
4✔
1029
                        CheckLeadingWhitespace,
4✔
1030
                )
4✔
1031
        }
4✔
1032
}
1033

1034
func (w *WSL) checkTrailingNewline(body *ast.BlockStmt) {
709✔
1035
        if _, ok := w.config.Checks[CheckTrailingWhitespace]; !ok {
961✔
1036
                return
252✔
1037
        }
252✔
1038

1039
        if len(body.List) == 0 {
467✔
1040
                return
10✔
1041
        }
10✔
1042

1043
        lastStmt := body.List[len(body.List)-1]
447✔
1044

447✔
1045
        // We don't want to force removal of the empty line for the last case since
447✔
1046
        // it can be used for consistency and readability.
447✔
1047
        if _, ok := lastStmt.(*ast.CaseClause); ok {
484✔
1048
                return
37✔
1049
        }
37✔
1050

1051
        lastContentPos := lastStmt.End()
410✔
1052

410✔
1053
        // Empty label statements need positional adjustment. #92
410✔
1054
        if l, ok := lastStmt.(*ast.LabeledStmt); ok {
412✔
1055
                if _, ok := l.Stmt.(*ast.EmptyStmt); ok {
3✔
1056
                        lastContentPos = lastStmt.Pos()
1✔
1057
                }
1✔
1058
        }
1059

1060
        // Find the last comment after last statement using position comparison.
1061
        for _, cg := range w.file.Comments {
3,419✔
1062
                if cg.End() <= lastContentPos {
5,631✔
1063
                        continue
2,622✔
1064
                }
1065

1066
                if cg.Pos() >= body.Rbrace {
736✔
1067
                        break
349✔
1068
                }
1069

1070
                if cg.End() < body.Rbrace {
76✔
1071
                        lastContentPos = cg.End()
38✔
1072
                }
38✔
1073
        }
1074

1075
        closingLine := w.lineFor(body.Rbrace)
410✔
1076
        lastContentLine := w.lineFor(lastContentPos)
410✔
1077

410✔
1078
        if closingLine > lastContentLine+1 {
423✔
1079
                file := w.fset.File(body.Rbrace)
13✔
1080
                removeStart := file.LineStart(lastContentLine + 1)
13✔
1081
                removeEnd := file.LineStart(closingLine)
13✔
1082
                w.addErrorRemoveNewline(removeStart, removeEnd, CheckTrailingWhitespace)
13✔
1083
        }
13✔
1084
}
1085

1086
func (w *WSL) maybeGroupDecl(stmt *ast.DeclStmt, cursor *Cursor) bool {
29✔
1087
        firstNode := asGenDeclWithValueSpecs(cursor.PreviousNode())
29✔
1088
        if firstNode == nil {
36✔
1089
                return false
7✔
1090
        }
7✔
1091

1092
        currentNode := asGenDeclWithValueSpecs(stmt)
22✔
1093
        if currentNode == nil {
25✔
1094
                return false
3✔
1095
        }
3✔
1096

1097
        // Both are not same type, e.g. `const` or `var`
1098
        if firstNode.Tok != currentNode.Tok {
19✔
1099
                return false
×
1100
        }
×
1101

1102
        group := &ast.GenDecl{
19✔
1103
                Tok:    firstNode.Tok,
19✔
1104
                Lparen: 1,
19✔
1105
                Specs:  firstNode.Specs,
19✔
1106
        }
19✔
1107

19✔
1108
        group.Specs = append(group.Specs, currentNode.Specs...)
19✔
1109

19✔
1110
        reportNodes := []ast.Node{currentNode}
19✔
1111
        lastNode := currentNode
19✔
1112

19✔
1113
        for {
46✔
1114
                nextPeeked := cursor.NextNode()
27✔
1115
                if nextPeeked == nil {
27✔
1116
                        break
×
1117
                }
1118

1119
                if w.lineFor(lastNode.End()) < w.lineFor(nextPeeked.Pos())-1 {
37✔
1120
                        break
10✔
1121
                }
1122

1123
                nextNode := asGenDeclWithValueSpecs(nextPeeked)
17✔
1124
                if nextNode == nil {
26✔
1125
                        break
9✔
1126
                }
1127

1128
                if nextNode.Tok != firstNode.Tok {
8✔
1129
                        break
×
1130
                }
1131

1132
                cursor.Next()
8✔
1133

8✔
1134
                group.Specs = append(group.Specs, nextNode.Specs...)
8✔
1135
                reportNodes = append(reportNodes, nextNode)
8✔
1136
                lastNode = nextNode
8✔
1137
        }
1138

1139
        var buf bytes.Buffer
19✔
1140
        if err := format.Node(&buf, token.NewFileSet(), group); err != nil {
19✔
1141
                return false
×
1142
        }
×
1143

1144
        // We add a diagnostic to every subsequent statement to properly represent
1145
        // the violations. Duplicate fixes for the same range is fine.
1146
        for _, n := range reportNodes {
46✔
1147
                w.addErrorWithMessageAndFix(
27✔
1148
                        n.Pos(),
27✔
1149
                        firstNode.Pos(),
27✔
1150
                        lastNode.End(),
27✔
1151
                        fmt.Sprintf("%s (never cuddle %s)", messageMissingWhitespaceAbove, CheckDecl),
27✔
1152
                        buf.Bytes(),
27✔
1153
                )
27✔
1154
        }
27✔
1155

1156
        return true
19✔
1157
}
1158

1159
func (w *WSL) maybeCheckBlock(
1160
        node ast.Node,
1161
        blockStmt *ast.BlockStmt,
1162
        cursor *Cursor,
1163
        check CheckType,
1164
) {
145✔
1165
        w.checkBlock(blockStmt, cursor)
145✔
1166

145✔
1167
        if _, ok := w.config.Checks[check]; ok {
224✔
1168
                cursor.SetChecker(check)
79✔
1169

79✔
1170
                var (
79✔
1171
                        blockList     []ast.Stmt
79✔
1172
                        allowedIdents []*ast.Ident
79✔
1173
                )
79✔
1174

79✔
1175
                if check != CheckSwitch && check != CheckTypeSwitch && check != CheckSelect {
115✔
1176
                        blockList = blockStmt.List
36✔
1177
                } else {
79✔
1178
                        allowedIdents = w.identsFromCaseArms(node)
43✔
1179
                }
43✔
1180

1181
                w.checkCuddlingBlock(node, blockList, allowedIdents, cursor)
79✔
1182
        }
1183
}
1184

1185
func (w *WSL) maybeCheckExpr(
1186
        node ast.Node,
1187
        cursor *Cursor,
1188
        predicate func(ast.Node) (bool, bool),
1189
        check CheckType,
1190
) {
435✔
1191
        if _, ok := w.config.Checks[check]; ok {
638✔
1192
                cursor.SetChecker(check)
203✔
1193
                previousNode := cursor.PreviousNode()
203✔
1194

203✔
1195
                if enforceLimit, shouldCheck := predicate(previousNode); shouldCheck {
383✔
1196
                        w.checkCuddling(node, cursor, enforceLimit)
180✔
1197
                }
180✔
1198
        }
1199
}
1200

1201
// numberOfStatementsAbove will find out how many lines above the cursor's
1202
// current statement there is without any newlines between.
1203
func (w *WSL) numberOfStatementsAbove(cursor *Cursor) int {
1,493✔
1204
        defer cursor.Save()()
1,493✔
1205

1,493✔
1206
        statementsWithoutNewlines := 0
1,493✔
1207
        currentStmtStartLine := w.lineFor(cursor.Stmt().Pos())
1,493✔
1208

1,493✔
1209
        for cursor.Previous() {
2,771✔
1210
                previousStmtEndLine := w.lineFor(cursor.Stmt().End())
1,278✔
1211
                if previousStmtEndLine != currentStmtStartLine-1 {
1,691✔
1212
                        break
413✔
1213
                }
1214

1215
                currentStmtStartLine = w.lineFor(cursor.Stmt().Pos())
865✔
1216
                statementsWithoutNewlines++
865✔
1217
        }
1218

1219
        return statementsWithoutNewlines
1,493✔
1220
}
1221

1222
func (w *WSL) lineFor(pos token.Pos) int {
7,852✔
1223
        return w.fset.PositionFor(pos, false).Line
7,852✔
1224
}
7,852✔
1225

1226
func (w *WSL) lineStartOf(pos token.Pos) token.Pos {
229✔
1227
        return w.fset.File(pos).LineStart(w.lineFor(pos))
229✔
1228
}
229✔
1229

1230
func (w *WSL) implementsErr(node *ast.Ident) bool {
21✔
1231
        typeInfo := w.typeInfo.TypeOf(node)
21✔
1232
        if typeInfo == nil {
21✔
1233
                return false
×
1234
        }
×
1235

1236
        errorType, ok := types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
21✔
1237
        if !ok {
21✔
1238
                return false
×
1239
        }
×
1240

1241
        return types.Implements(typeInfo, errorType)
21✔
1242
}
1243

1244
func (w *WSL) commentOnLineAfterNodePos(node ast.Node) token.Pos {
39✔
1245
        nodeEndLine := w.lineFor(node.End())
39✔
1246

39✔
1247
        for _, cg := range w.file.Comments {
768✔
1248
                if cg.End() <= node.End() {
1,419✔
1249
                        continue
690✔
1250
                }
1251

1252
                commentLine := w.lineFor(cg.Pos())
39✔
1253
                if commentLine == nodeEndLine {
42✔
1254
                        continue
3✔
1255
                }
1256

1257
                if commentLine == nodeEndLine+1 {
39✔
1258
                        return cg.Pos()
3✔
1259
                }
3✔
1260

1261
                break
33✔
1262
        }
1263

1264
        return token.NoPos
36✔
1265
}
1266

1267
func (w *WSL) addErrorInvalidTypeCuddle(pos token.Pos, ct CheckType) {
37✔
1268
        reportMessage := fmt.Sprintf("%s (invalid statement above %s)", messageMissingWhitespaceAbove, ct)
37✔
1269
        insertPos := w.lineStartOf(pos)
37✔
1270
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
37✔
1271
}
37✔
1272

1273
func (w *WSL) addErrorTooManyStatements(pos token.Pos, ct CheckType) {
22✔
1274
        reportMessage := fmt.Sprintf("%s (too many statements above %s)", messageMissingWhitespaceAbove, ct)
22✔
1275
        insertPos := w.lineStartOf(pos)
22✔
1276
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
22✔
1277
}
22✔
1278

1279
func (w *WSL) addErrorNoIntersection(pos token.Pos, ct CheckType) {
35✔
1280
        reportMessage := fmt.Sprintf("%s (no shared variables above %s)", messageMissingWhitespaceAbove, ct)
35✔
1281
        insertPos := w.lineStartOf(pos)
35✔
1282
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
35✔
1283
}
35✔
1284

1285
func (w *WSL) addErrorVariableNotShared(pos token.Pos, ct CheckType) {
10✔
1286
        reportMessage := fmt.Sprintf("%s (variable not shared with %s)", messageMissingWhitespaceAbove, ct)
10✔
1287
        insertPos := w.lineStartOf(pos)
10✔
1288
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
10✔
1289
}
10✔
1290

1291
func (w *WSL) addErrorTooManyLines(pos token.Pos, ct CheckType) {
17✔
1292
        reportMessage := fmt.Sprintf("%s (too many lines above %s)", messageMissingWhitespaceAbove, ct)
17✔
1293
        insertPos := w.lineStartOf(pos)
17✔
1294
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
17✔
1295
}
17✔
1296

1297
func (w *WSL) addErrorNeverAllow(pos token.Pos, ct CheckType) {
15✔
1298
        reportMessage := fmt.Sprintf("%s (never cuddle %s)", messageMissingWhitespaceAbove, ct)
15✔
1299
        insertPos := w.lineStartOf(pos)
15✔
1300
        w.addErrorWithMessage(pos, insertPos, insertPos, reportMessage)
15✔
1301
}
15✔
1302

1303
func (w *WSL) addError(report, start, end token.Pos, message string, ct CheckType) {
93✔
1304
        reportMessage := fmt.Sprintf("%s (%s)", message, ct)
93✔
1305
        w.addErrorWithMessage(report, start, end, reportMessage)
93✔
1306
}
93✔
1307

1308
func (w *WSL) addErrorRemoveNewline(start, end token.Pos, ct CheckType) {
58✔
1309
        reportMessage := fmt.Sprintf("%s (%s)", messageRemoveWhitespace, ct)
58✔
1310
        w.addErrorWithMessageAndFix(start, start, end, reportMessage, []byte{})
58✔
1311
}
58✔
1312

1313
func (w *WSL) addErrorWithMessage(report, start, end token.Pos, message string) {
229✔
1314
        w.addErrorWithMessageAndFix(report, start, end, message, []byte("\n"))
229✔
1315
}
229✔
1316

1317
func (w *WSL) addErrorWithMessageAndFix(report, start, end token.Pos, message string, fix []byte) {
314✔
1318
        iss, ok := w.issues[report]
314✔
1319
        if !ok {
620✔
1320
                iss = issue{
306✔
1321
                        message:   message,
306✔
1322
                        fixRanges: []fixRange{},
306✔
1323
                }
306✔
1324
        }
306✔
1325

1326
        // Don't add a fix range that overlaps with an existing one — that would
1327
        // produce conflicting TextEdits. The existing fix already covers this range,
1328
        // so the diagnostic is surfaced via the first reporter.
1329
        for _, existing := range iss.fixRanges {
322✔
1330
                if start < existing.fixRangeEnd && end > existing.fixRangeStart {
12✔
1331
                        return
4✔
1332
                }
4✔
1333
        }
1334

1335
        iss.fixRanges = append(iss.fixRanges, fixRange{
310✔
1336
                fixRangeStart: start,
310✔
1337
                fixRangeEnd:   end,
310✔
1338
                fix:           fix,
310✔
1339
        })
310✔
1340

310✔
1341
        w.issues[report] = iss
310✔
1342
}
1343

1344
func isAssignDeclOrIncDec(n ast.Node) bool {
269✔
1345
        _, a := n.(*ast.AssignStmt)
269✔
1346
        _, d := n.(*ast.DeclStmt)
269✔
1347
        _, i := n.(*ast.IncDecStmt)
269✔
1348

269✔
1349
        return a || d || i
269✔
1350
}
269✔
1351

1352
func asGenDeclWithValueSpecs(n ast.Node) *ast.GenDecl {
68✔
1353
        decl, ok := n.(*ast.DeclStmt)
68✔
1354
        if !ok {
81✔
1355
                return nil
13✔
1356
        }
13✔
1357

1358
        genDecl, ok := decl.Decl.(*ast.GenDecl)
55✔
1359
        if !ok {
55✔
1360
                return nil
×
1361
        }
×
1362

1363
        for _, spec := range genDecl.Specs {
114✔
1364
                // We only care about value specs and not type specs or import
59✔
1365
                // specs. We will never see any import specs but type specs we just
59✔
1366
                // separate with an empty line as usual.
59✔
1367
                valueSpec, ok := spec.(*ast.ValueSpec)
59✔
1368
                if !ok {
60✔
1369
                        return nil
1✔
1370
                }
1✔
1371

1372
                // It's very hard to get comments right in the ast and with the current
1373
                // way the ast package works we simply don't support grouping at all if
1374
                // there are any comments related to the node.
1375
                if valueSpec.Doc != nil || valueSpec.Comment != nil {
63✔
1376
                        return nil
5✔
1377
                }
5✔
1378
        }
1379

1380
        return genDecl
49✔
1381
}
1382

1383
func (w *WSL) hasIntersection(a, b ast.Node) bool {
16✔
1384
        aI := w.identsFromNode(a, true)
16✔
1385
        bI := w.identsFromNode(b, true)
16✔
1386

16✔
1387
        return identsIntersect(aI, bI)
16✔
1388
}
16✔
1389

1390
func identsIntersect(a, b []*ast.Ident) bool {
277✔
1391
        for _, as := range a {
570✔
1392
                for _, bs := range b {
860✔
1393
                        if as.Name == bs.Name {
791✔
1394
                                return true
224✔
1395
                        }
224✔
1396
                }
1397
        }
1398

1399
        return false
53✔
1400
}
1401

1402
func isTypeOrPredeclConst(obj types.Object) bool {
1,019✔
1403
        switch o := obj.(type) {
1,019✔
1404
        case *types.TypeName:
52✔
1405
                // Covers predeclared types ("string", "int", ...) and user types.
52✔
1406
                return true
52✔
1407
        case *types.Const:
82✔
1408
                // true/false/iota are universe consts.
82✔
1409
                return o.Parent() == types.Universe
82✔
1410
        case *types.Nil:
7✔
1411
                return true
7✔
1412
        case *types.PkgName:
55✔
1413
                // Skip package qualifiers like "fmt" in fmt.Println
55✔
1414
                return true
55✔
1415
        default:
823✔
1416
                return false
823✔
1417
        }
1418
}
1419

1420
// identsFromNode returns all *ast.Ident in a node except:
1421
//   - type names (types.TypeName)
1422
//   - builtin constants from the universe (true, false, iota)
1423
//   - nil (*types.Nil)
1424
//   - package names (types.PkgName)
1425
//   - the blank identifier "_"
1426
func (w *WSL) identsFromNode(node ast.Node, skipBlock bool) []*ast.Ident {
858✔
1427
        var (
858✔
1428
                idents []*ast.Ident
858✔
1429
                seen   = map[string]struct{}{}
858✔
1430
        )
858✔
1431

858✔
1432
        if node == nil {
1,079✔
1433
                return idents
221✔
1434
        }
221✔
1435

1436
        addIdent := func(ident *ast.Ident) {
1,473✔
1437
                if ident == nil {
836✔
1438
                        return
×
1439
                }
×
1440

1441
                name := ident.Name
836✔
1442
                if name == "" || name == "_" {
854✔
1443
                        return
18✔
1444
                }
18✔
1445

1446
                if _, ok := seen[name]; ok {
857✔
1447
                        return
39✔
1448
                }
39✔
1449

1450
                idents = append(idents, ident)
779✔
1451
                seen[name] = struct{}{}
779✔
1452
        }
1453

1454
        ast.Inspect(node, func(n ast.Node) bool {
6,048✔
1455
                if skipBlock {
10,322✔
1456
                        if _, ok := n.(*ast.BlockStmt); ok {
5,036✔
1457
                                return false
125✔
1458
                        }
125✔
1459
                }
1460

1461
                ident, ok := n.(*ast.Ident)
5,286✔
1462
                if !ok {
9,540✔
1463
                        return true
4,254✔
1464
                }
4,254✔
1465

1466
                // Prefer Uses over Defs; fall back to Defs if not a use site.
1467
                var typesObject types.Object
1,032✔
1468
                if obj := w.typeInfo.Uses[ident]; obj != nil {
1,721✔
1469
                        typesObject = obj
689✔
1470
                } else if obj := w.typeInfo.Defs[ident]; obj != nil {
1,362✔
1471
                        typesObject = obj
330✔
1472
                }
330✔
1473

1474
                // Unresolved (could be a build-tag or syntax artifact). Keep it.
1475
                if typesObject == nil {
1,045✔
1476
                        addIdent(ident)
13✔
1477
                        return true
13✔
1478
                }
13✔
1479

1480
                if isTypeOrPredeclConst(typesObject) {
1,215✔
1481
                        return true
196✔
1482
                }
196✔
1483

1484
                addIdent(ident)
823✔
1485

823✔
1486
                return true
823✔
1487
        })
1488

1489
        return idents
637✔
1490
}
1491

1492
func (w *WSL) identsFromCaseArms(node ast.Node) []*ast.Ident {
43✔
1493
        var (
43✔
1494
                idents []*ast.Ident
43✔
1495
                nodes  []ast.Stmt
43✔
1496
                seen   = map[string]struct{}{}
43✔
1497

43✔
1498
                addUnseen = func(node ast.Node) {
136✔
1499
                        for _, ident := range w.identsFromNode(node, true) {
127✔
1500
                                if _, ok := seen[ident.Name]; ok {
40✔
1501
                                        continue
6✔
1502
                                }
1503

1504
                                seen[ident.Name] = struct{}{}
28✔
1505
                                idents = append(idents, ident)
28✔
1506
                        }
1507
                }
1508
        )
1509

1510
        switch v := node.(type) {
43✔
1511
        case *ast.SwitchStmt:
31✔
1512
                nodes = v.Body.List
31✔
1513
        case *ast.TypeSwitchStmt:
6✔
1514
                nodes = v.Body.List
6✔
1515
        case *ast.SelectStmt:
6✔
1516
                nodes = v.Body.List
6✔
1517
        default:
×
1518
                return idents
×
1519
        }
1520

1521
        for _, node := range nodes {
143✔
1522
                switch n := node.(type) {
100✔
1523
                case *ast.CommClause:
16✔
1524
                        addUnseen(n.Comm)
16✔
1525
                case *ast.CaseClause:
84✔
1526
                        for _, n := range n.List {
161✔
1527
                                addUnseen(n)
77✔
1528
                        }
77✔
1529
                default:
×
1530
                        continue
×
1531
                }
1532
        }
1533

1534
        return idents
43✔
1535
}
1536

1537
// hasSelectorCall checks if node contains a selector call with one of the given names.
1538
func hasSelectorCall(node ast.Node, selectorNames []string) bool {
167✔
1539
        var found bool
167✔
1540

167✔
1541
        ast.Inspect(node, func(n ast.Node) bool {
1,462✔
1542
                if found {
1,315✔
1543
                        return false // Already found
20✔
1544
                }
20✔
1545

1546
                if _, ok := n.(*ast.BlockStmt); ok {
1,294✔
1547
                        return false
19✔
1548
                }
19✔
1549

1550
                if sel, ok := n.(*ast.SelectorExpr); ok {
1,284✔
1551
                        found = slices.Contains(selectorNames, sel.Sel.Name)
28✔
1552
                        return false
28✔
1553
                }
28✔
1554

1555
                return true
1,228✔
1556
        })
1557

1558
        return found
167✔
1559
}
1560

1561
func (w *WSL) isLockOrUnlock(current, previous ast.Node) bool {
167✔
1562
        // If we're an ExprStmt (e.g. X()), we check if we're calling `Unlock` or
167✔
1563
        // `RWUnlock`. No matter how deep this is or what previous statement was, we
167✔
1564
        // allow this.
167✔
1565
        //
167✔
1566
        // mu.Lock()
167✔
1567
        // [ANY BLOCK]
167✔
1568
        // mu.Unlock()
167✔
1569
        if _, ok := current.(*ast.ExprStmt); ok {
184✔
1570
                return hasSelectorCall(current, []string{"Unlock", "RWUnlock"})
17✔
1571
        }
17✔
1572

1573
        if previous != nil {
300✔
1574
                return hasSelectorCall(previous, []string{"Lock", "RWLock", "TryLock"})
150✔
1575
        }
150✔
1576

1577
        return false
×
1578
}
1579

1580
// isErrNotNilCheck returns the error identifier if stmt is an `if err != nil`
1581
// or `if err == nil` check without an init statement, nil otherwise.
1582
func (w *WSL) isErrNotNilCheck(stmt ast.Node) *ast.Ident {
314✔
1583
        ifStmt, ok := stmt.(*ast.IfStmt)
314✔
1584
        if !ok {
513✔
1585
                return nil
199✔
1586
        }
199✔
1587

1588
        // If the error checking has an init condition (e.g. if err := f();) we
1589
        // don't consider it an error check since the error is assigned on this row.
1590
        if ifStmt.Init != nil {
120✔
1591
                return nil
5✔
1592
        }
5✔
1593

1594
        // The condition must be a binary expression (X OP Y)
1595
        binaryExpr, ok := ifStmt.Cond.(*ast.BinaryExpr)
110✔
1596
        if !ok {
154✔
1597
                return nil
44✔
1598
        }
44✔
1599

1600
        // We must do not equal or equal comparison (!= or ==)
1601
        if binaryExpr.Op != token.NEQ && binaryExpr.Op != token.EQL {
110✔
1602
                return nil
44✔
1603
        }
44✔
1604

1605
        xIdent, ok := binaryExpr.X.(*ast.Ident)
22✔
1606
        if !ok {
23✔
1607
                return nil
1✔
1608
        }
1✔
1609

1610
        // X is not an error so it's not error checking
1611
        if !w.implementsErr(xIdent) {
27✔
1612
                return nil
6✔
1613
        }
6✔
1614

1615
        yIdent, ok := binaryExpr.Y.(*ast.Ident)
15✔
1616
        if !ok {
15✔
1617
                return nil
×
1618
        }
×
1619

1620
        // Y is not compared with `nil`
1621
        if yIdent.Name != "nil" {
15✔
1622
                return nil
×
1623
        }
×
1624

1625
        return xIdent
15✔
1626
}
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

© 2026 Coveralls, Inc