Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 12:15 UTC
caddyfile: Improve import/global options UX for imports before global options (#7642)
* caddyfile: improve import/global options UX Keep standalone global-options braces stable in fmt when they follow import lines. Also improve validate output for imports before the global options block with a clearer error message. Add focused formatter and parser regression coverage * caddyfile: satisfy staticcheck in formatter
Zen Dodd committed
Apr 11, 2026 at 09:17 UTC
8e2dd5079c27af50a3288502b3a82f683e3737fe
4 files changed
+101
-1
caddyconfig/caddyfile/formatter.go
+39
-1
@@ -63,8 +63,33 @@ func Format(input []byte) []byte {
63
heredocClosingMarker []rune
64
65
nesting int // indentation level
66
+
67
+ currentToken strings.Builder
68
+ currentLineFirstToken string
69
+ previousLineWasTopLevelImport bool
70
+ openBraceOwnLine bool
71
)
72
73
+ finishToken := func() {
74
+ if currentToken.Len() == 0 {
75
+ return
76
+ }
77
+ if currentLineFirstToken == "" {
78
+ currentLineFirstToken = currentToken.String()
79
+ }
80
+ currentToken.Reset()
81
+ }
82
+
83
+ finishLine := func() {
84
+ finishToken()
85
+ if currentLineFirstToken != "" {
86
+ previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
87
+ } else if !openBrace || !openBraceOwnLine || openBraceWritten {
88
+ previousLineWasTopLevelImport = false
89
+ }
90
+ currentLineFirstToken = ""
91
+ }
92
+
93
write := func(ch rune) {
94
out.WriteRune(ch)
95
last = ch
@@ -220,9 +245,11 @@ func Format(input []byte) []byte {
245
}
246
247
if unicode.IsSpace(ch) {
248
+ finishToken()
249
space = true
250
heredocEscaped = false
251
if ch == '\n' {
252
+ finishLine()
253
newLines++
254
}
255
continue
@@ -249,13 +276,19 @@ func Format(input []byte) []byte {
276
}
277
278
openBrace = false
252
- if beginningOfLine {
279
+ if openBraceOwnLine && previousLineWasTopLevelImport {
280
+ if last != '\n' {
281
+ nextLine()
282
+ }
283
+ indent()
284
+ } else if beginningOfLine {
285
indent()
286
} else if !openBraceSpace || !unicode.IsSpace(last) {
287
write(' ')
288
}
289
write('{')
290
openBraceWritten = true
291
+ openBraceOwnLine = false
292
nextLine()
293
newLines = 0
294
// prevent infinite nesting from ridiculous inputs (issue #4169)
@@ -266,8 +299,10 @@ func Format(input []byte) []byte {
299
300
switch {
301
case ch == '{':
302
+ finishToken()
303
openBrace = true
304
openBraceSpace = spacePrior && !beginningOfLine
305
+ openBraceOwnLine = newLines > 0
306
if openBraceSpace && newLines == 0 {
307
write(' ')
308
}
@@ -275,11 +310,13 @@ func Format(input []byte) []byte {
310
if quotes == "`" {
311
write('{')
312
openBraceWritten = true
313
+ openBraceOwnLine = false
314
continue
315
}
316
continue
317
318
case ch == '}' && (spacePrior || !openBrace):
319
+ finishToken()
320
if quotes == "`" {
321
write('}')
322
continue
@@ -324,6 +361,7 @@ func Format(input []byte) []byte {
361
space = true
362
}
363
364
+ currentToken.WriteRune(ch)
365
write(ch)
366
367
beginningOfLine = false
caddyconfig/caddyfile/formatter_test.go
+15
@@ -475,6 +475,21 @@ Hope this helps.` + "`" + `
475
}`,
476
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
477
},
478
+ {
479
+ description: "imports before global options block keep standalone brace",
480
+ input: `import ./conf.d/matcher_my_subnet.caddy
481
+import ./conf.d/matcher_not_my_subnet.caddy
482
+{
483
+ order crowdsec first
484
+ order appsec after crowdsec
485
+}`,
486
+ expect: `import ./conf.d/matcher_my_subnet.caddy
487
+import ./conf.d/matcher_not_my_subnet.caddy
488
+{
489
+ order crowdsec first
490
+ order appsec after crowdsec
491
+}`,
492
+ },
493
} {
494
// the formatter should output a trailing newline,
495
// even if the tests aren't written to expect that
caddyconfig/caddyfile/parse.go
+17
@@ -682,11 +682,28 @@ func (p *parser) directive() error {
682
// a opening curly brace. It does NOT advance the token.
683
func (p *parser) openCurlyBrace() error {
684
if p.Val() != "{" {
685
+ if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
686
+ return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
687
+ }
688
return p.SyntaxErr("{")
689
}
690
return nil
691
}
692
693
+func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
694
+ if p.Val() != "import" || len(p.block.Keys) == 0 {
695
+ return false
696
+ }
697
+
698
+ for _, key := range p.block.Keys {
699
+ if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
700
+ return false
701
+ }
702
+ }
703
+
704
+ return true
705
+}
706
+
707
// closeCurlyBrace expects the current token to be
708
// a closing curly brace. This acts like an assertion
709
// because it returns an error if the token is not
caddyconfig/caddyfile/parse_test.go
+30
@@ -930,6 +930,36 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
930
}
931
}
932
933
+func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
934
+ tempDir := t.TempDir()
935
+ importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
936
+ importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
937
+
938
+ err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
939
+ if err != nil {
940
+ t.Fatalf("writing first import file: %v", err)
941
+ }
942
+
943
+ err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
944
+ if err != nil {
945
+ t.Fatalf("writing second import file: %v", err)
946
+ }
947
+
948
+ _, err = Parse("Testfile", []byte(`import `+importFile1+`
949
+import `+importFile2+`
950
+{
951
+ debug
952
+}`))
953
+ if err == nil {
954
+ t.Fatal("Expected an error, but got nil")
955
+ }
956
+
957
+ expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
958
+ if !strings.HasPrefix(err.Error(), expected) {
959
+ t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
960
+ }
961
+}
962
+
963
func testParser(input string) parser {
964
return parser{Dispenser: NewTestDispenser(input)}
965
}