@marcel91200 / caddy Mirror · read-only / commits / 8e2dd507
Fork 0

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 }