Skip to content

Commit 666f5bb

Browse files
committed
formatter: separate select case clauses
Treat select communication clauses like switch case clauses in the blank-line pass so separator blanks appear between clauses rather than inside the clause header. Suppress blank-before-return insertion when a case or select clause contains only a return statement, even when that return has a leading comment. Add a regression test for select clauses that keeps the first case compact, separates the next case, and preserves the existing blank-before-return behavior for a multi-statement final case.
1 parent 437e389 commit 666f5bb

6 files changed

Lines changed: 222 additions & 9 deletions

File tree

dsl/action.go

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5163,6 +5163,18 @@ func (c *blankLineBatchContext) initConditions() {
51635163
}
51645164

51655165
func (c *blankLineBatchContext) maybeInsertBlankBefore(n ast.Node) {
5166+
c.maybeInsertBlankBeforeNode(n, true)
5167+
}
5168+
5169+
func (c *blankLineBatchContext) maybeInsertBlankBeforeClause(n ast.Node) {
5170+
c.maybeInsertBlankBeforeNode(
5171+
n, shouldAttachLeadingCommentsForClause(c.ctx, n),
5172+
)
5173+
}
5174+
5175+
func (c *blankLineBatchContext) maybeInsertBlankBeforeNode(n ast.Node,
5176+
attachLeadingComments bool) {
5177+
51665178
if n == nil {
51675179
return
51685180
}
@@ -5171,7 +5183,9 @@ func (c *blankLineBatchContext) maybeInsertBlankBefore(n ast.Node) {
51715183
return
51725184
}
51735185
ls := lineStart(c.ctx.Source, start)
5174-
ls = leadingCommentBlockLineStart(c.ctx.Source, ls)
5186+
if attachLeadingComments {
5187+
ls = leadingCommentBlockLineStart(c.ctx.Source, ls)
5188+
}
51755189
if ls <= 0 || hasBlankLineBeforeLineStart(c.ctx.Source, ls) {
51765190
return
51775191
}
@@ -5191,7 +5205,13 @@ func (c *blankLineBatchContext) inspectFile(file *ast.File) {
51915205
case *ast.CaseClause:
51925206
c.maybeRemoveBlankAfterSingleLineBlockHeader(n)
51935207
if c.caseCond.Eval(caps, c.ctx) {
5194-
c.maybeInsertBlankBefore(n)
5208+
c.maybeInsertBlankBeforeClause(n)
5209+
}
5210+
5211+
case *ast.CommClause:
5212+
c.maybeRemoveBlankAfterSingleLineBlockHeader(n)
5213+
if c.caseCond.Eval(caps, c.ctx) {
5214+
c.maybeInsertBlankBeforeClause(n)
51955215
}
51965216

51975217
case *ast.ReturnStmt:
@@ -5275,6 +5295,14 @@ func (c *blankLineBatchContext) maybeRemoveBlankAfterSingleLineBlockHeader(
52755295
headerStart = c.ctx.Fset.Position(n.Pos())
52765296
headerEnd = c.ctx.Fset.Position(n.Colon)
52775297

5298+
case *ast.CommClause:
5299+
if n == nil || len(n.Body) == 0 {
5300+
return
5301+
}
5302+
first = n.Body[0]
5303+
headerStart = c.ctx.Fset.Position(n.Pos())
5304+
headerEnd = c.ctx.Fset.Position(n.Colon)
5305+
52785306
default:
52795307
return
52805308
}
@@ -5468,6 +5496,79 @@ func (a *InsertBlankBeforeAction) Execute(caps Captures, ctx *Context) ([]byte,
54685496
return out, true
54695497
}
54705498

5499+
// InsertBlankBeforeClauseAction inserts a blank line before a switch/select
5500+
// clause label.
5501+
type InsertBlankBeforeClauseAction struct {
5502+
Target string
5503+
}
5504+
5505+
// Execute implements Action for InsertBlankBeforeClauseAction.
5506+
func (a *InsertBlankBeforeClauseAction) Execute(caps Captures, ctx *Context) (
5507+
[]byte, bool) {
5508+
5509+
node := resolveTarget(caps, a.Target)
5510+
if node == nil {
5511+
return nil, false
5512+
}
5513+
5514+
pos := ctx.Fset.Position(node.Pos())
5515+
ls := lineStart(ctx.Source, pos.Offset)
5516+
if shouldAttachLeadingCommentsForClause(ctx, node) {
5517+
ls = leadingCommentBlockLineStart(ctx.Source, ls)
5518+
}
5519+
if hasBlankLineBeforeLineStart(ctx.Source, ls) {
5520+
return nil, false
5521+
}
5522+
5523+
out, err := ApplySingleEdit(ctx.Source, ls, ls, []byte("\n"))
5524+
if err != nil {
5525+
return nil, false
5526+
}
5527+
5528+
return out, true
5529+
}
5530+
5531+
func shouldAttachLeadingCommentsForClause(ctx *Context, node ast.Node) bool {
5532+
parent, ok := ctx.Parent(node).(*ast.BlockStmt)
5533+
if !ok || parent == nil {
5534+
return true
5535+
}
5536+
5537+
for i, stmt := range parent.List {
5538+
if stmt != node {
5539+
continue
5540+
}
5541+
if i == 0 {
5542+
return true
5543+
}
5544+
5545+
return clauseBodyLen(parent.List[i-1]) > 0
5546+
}
5547+
5548+
return true
5549+
}
5550+
5551+
func clauseBodyLen(stmt ast.Stmt) int {
5552+
switch n := stmt.(type) {
5553+
case *ast.CaseClause:
5554+
if n == nil {
5555+
return 0
5556+
}
5557+
5558+
return len(n.Body)
5559+
5560+
case *ast.CommClause:
5561+
if n == nil {
5562+
return 0
5563+
}
5564+
5565+
return len(n.Body)
5566+
5567+
default:
5568+
return 0
5569+
}
5570+
}
5571+
54715572
// InsertBlankAfterAction inserts a blank line after a node if not already
54725573
// present.
54735574
type InsertBlankAfterAction struct {

dsl/condition.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ func (c *ExprEditSafeCond) Eval(caps Captures, ctx *Context) bool {
215215
case CallArgsPolicyForce:
216216

217217
// OK.
218+
218219
case CallArgsPolicyAuto:
219220
call := enclosingCallForArg(node, ctx)
220221
if call == nil {
@@ -1103,8 +1104,8 @@ func (c *IsFinalReturnCond) Eval(caps Captures, ctx *Context) bool {
11031104
}
11041105
}
11051106

1106-
// HasPrecedingSiblingCond checks if a case clause has a preceding sibling case.
1107-
// Used to determine if blank line is needed before a case.
1107+
// HasPrecedingSiblingCond checks if a switch/select clause has a preceding
1108+
// sibling clause. Used to determine if a blank line is needed before a clause.
11081109
type HasPrecedingSiblingCond struct {
11091110
Target string
11101111
}
@@ -1116,8 +1117,10 @@ func (c *HasPrecedingSiblingCond) Eval(caps Captures, ctx *Context) bool {
11161117
return false
11171118
}
11181119

1119-
caseClause, ok := node.(*ast.CaseClause)
1120-
if !ok {
1120+
switch node.(type) {
1121+
case *ast.CaseClause, *ast.CommClause:
1122+
1123+
default:
11211124
return false
11221125
}
11231126

@@ -1133,9 +1136,9 @@ func (c *HasPrecedingSiblingCond) Eval(caps Captures, ctx *Context) bool {
11331136
return false
11341137
}
11351138

1136-
// Find this case in the block's statements
1139+
// Find this clause in the block's statements.
11371140
for i, stmt := range blockStmt.List {
1138-
if stmt == caseClause {
1141+
if stmt == node {
11391142
return i > 0 // Has preceding sibling if not first
11401143
}
11411144
}
@@ -2115,6 +2118,9 @@ func (c *IsReturnNeedingBlankCond) Eval(caps Captures, ctx *Context) bool {
21152118
if !ok {
21162119
return false
21172120
}
2121+
if onlyStmtInClause(ctx, node) {
2122+
return false
2123+
}
21182124
if onlyStmtInBlockWithoutLeadingComment(ctx, node) {
21192125
return false
21202126
}
@@ -2167,6 +2173,21 @@ func (c *IsReturnNeedingBlankCond) Eval(caps Captures, ctx *Context) bool {
21672173
return true
21682174
}
21692175

2176+
func onlyStmtInClause(ctx *Context, node ast.Node) bool {
2177+
switch parent := ctx.Parent(node).(type) {
2178+
case *ast.CaseClause:
2179+
return parent != nil && len(parent.Body) == 1 &&
2180+
parent.Body[0] == node
2181+
2182+
case *ast.CommClause:
2183+
return parent != nil && len(parent.Body) == 1 &&
2184+
parent.Body[0] == node
2185+
2186+
default:
2187+
return false
2188+
}
2189+
}
2190+
21702191
func onlyStmtInBlockWithoutLeadingComment(ctx *Context, node ast.Node) bool {
21712192
block, ok := ctx.Parent(node).(*ast.BlockStmt)
21722193
if !ok || block == nil || len(block.List) != 1 ||

dsl/func_lit_expand_action.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func isTrivialInlineFuncLit(fn *ast.FuncLit) bool {
124124
case *ast.Ident, *ast.BasicLit:
125125

126126
// OK.
127+
127128
default:
128129
return false
129130
}

dsl/layout/render.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func RenderAt(doc Doc, colLimit int, tabStop int, indent string,
7070
case ForceBreak:
7171

7272
// No output; acts only as a fit-check barrier.
73+
7374
case Concat:
7475
// Push in reverse so we render in order.
7576
for i := len(d) - 1; i >= 0; i-- {
@@ -194,6 +195,7 @@ func fitsAt(doc Doc, remaining int, tabStop int, startCol int) bool {
194195
case SoftLine:
195196

196197
// Flat soft line is empty.
198+
197199
case ForceBreak:
198200
return false
199201

dsl/rules.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,24 @@ func BlankLineRulesWithOptions(opts BlankLineOptions) []Rule {
917917
},
918918
},
919919
Priority: 10,
920-
Action: &InsertBlankBeforeAction{
920+
Action: &InsertBlankBeforeClauseAction{
921+
Target: "node",
922+
},
923+
},
924+
{
925+
Name: "blank_before_comm_clause",
926+
Pattern: &NodePattern{
927+
Type: "CommClause",
928+
},
929+
When: &AndCond{
930+
Conds: []Condition{
931+
&HasPrecedingSiblingCond{
932+
Target: "node",
933+
},
934+
},
935+
},
936+
Priority: 10,
937+
Action: &InsertBlankBeforeClauseAction{
921938
Target: "node",
922939
},
923940
},

formatter/pipeline_dsl_blanklines_extras_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,77 @@ func record() {}
169169
require.NoError(t, err)
170170
}
171171

172+
func TestDSLBlankLinesNative_SelectCasesSeparateClausesNotHeaders(
173+
t *testing.T) {
174+
175+
const in = `package p
176+
177+
import "context"
178+
179+
func f(ctx context.Context, done chan struct{}) error {
180+
select {
181+
case <-done:
182+
// Work completed.
183+
return nil
184+
case <-ctx.Done():
185+
// Context cancelled before work finished.
186+
close(done)
187+
return ctx.Err()
188+
}
189+
}
190+
`
191+
192+
p := NewPipeline(
193+
PipelineConfig{
194+
ColumnLimit: 80,
195+
TabStop: 8,
196+
UseDSLBlankLines: true,
197+
UseDSLBlankLinesNative: true,
198+
},
199+
)
200+
201+
first := p.Format([]byte(in))
202+
second := p.Format(first)
203+
require.Equal(t, string(first), string(second))
204+
205+
out := string(first)
206+
require.Contains(
207+
t, out, "case <-done:\n // Work "+
208+
"completed.\n return nil\n\n case "+
209+
"<-ctx.Done():",
210+
)
211+
require.Contains(
212+
t, out,
213+
"close(done)\n\n\t\treturn ctx.Err()\n\t}",
214+
)
215+
require.NotContains(t, out, "case <-done:\n\n // Work "+
216+
"completed.")
217+
218+
const commentOnlyCase = `package p
219+
220+
func f(done chan struct{}, cancel chan struct{}) {
221+
select {
222+
case <-done:
223+
// Good.
224+
case <-cancel:
225+
return
226+
}
227+
}
228+
`
229+
230+
commentOnlyOut := string(p.Format([]byte(commentOnlyCase)))
231+
require.Contains(
232+
t, commentOnlyOut,
233+
"case <-done:\n // Good.\n\n case <-cancel:",
234+
)
235+
require.NotContains(t, commentOnlyOut, "case "+
236+
"<-done:\n\n // Good.")
237+
238+
fset := token.NewFileSet()
239+
_, err := parser.ParseFile(fset, "out.go", first, parser.AllErrors)
240+
require.NoError(t, err)
241+
}
242+
172243
func TestDSLBlankLinesNative_DoesNotBlankBeforeOnlyWrappedReturn(t *testing.T) {
173244
const in = `package p
174245

0 commit comments

Comments
 (0)