caddyfile: Improve import/global options UX for imports before global options (#7642)
Some checks failed
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 35s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 32s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m2s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m31s
Lint / dependency-review (push) Failing after 24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m20s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 33s
Lint / govulncheck (push) Successful in 1m23s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m17s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled

* 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
This commit is contained in:
Zen Dodd 2026-04-11 09:17:55 +10:00 committed by GitHub
parent 5f44ea0748
commit 8e2dd5079c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 1 deletions

View File

@ -63,8 +63,33 @@ func Format(input []byte) []byte {
heredocClosingMarker []rune
nesting int // indentation level
currentToken strings.Builder
currentLineFirstToken string
previousLineWasTopLevelImport bool
openBraceOwnLine bool
)
finishToken := func() {
if currentToken.Len() == 0 {
return
}
if currentLineFirstToken == "" {
currentLineFirstToken = currentToken.String()
}
currentToken.Reset()
}
finishLine := func() {
finishToken()
if currentLineFirstToken != "" {
previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
} else if !openBrace || !openBraceOwnLine || openBraceWritten {
previousLineWasTopLevelImport = false
}
currentLineFirstToken = ""
}
write := func(ch rune) {
out.WriteRune(ch)
last = ch
@ -220,9 +245,11 @@ func Format(input []byte) []byte {
}
if unicode.IsSpace(ch) {
finishToken()
space = true
heredocEscaped = false
if ch == '\n' {
finishLine()
newLines++
}
continue
@ -249,13 +276,19 @@ func Format(input []byte) []byte {
}
openBrace = false
if beginningOfLine {
if openBraceOwnLine && previousLineWasTopLevelImport {
if last != '\n' {
nextLine()
}
indent()
} else if beginningOfLine {
indent()
} else if !openBraceSpace || !unicode.IsSpace(last) {
write(' ')
}
write('{')
openBraceWritten = true
openBraceOwnLine = false
nextLine()
newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169)
@ -266,8 +299,10 @@ func Format(input []byte) []byte {
switch {
case ch == '{':
finishToken()
openBrace = true
openBraceSpace = spacePrior && !beginningOfLine
openBraceOwnLine = newLines > 0
if openBraceSpace && newLines == 0 {
write(' ')
}
@ -275,11 +310,13 @@ func Format(input []byte) []byte {
if quotes == "`" {
write('{')
openBraceWritten = true
openBraceOwnLine = false
continue
}
continue
case ch == '}' && (spacePrior || !openBrace):
finishToken()
if quotes == "`" {
write('}')
continue
@ -324,6 +361,7 @@ func Format(input []byte) []byte {
space = true
}
currentToken.WriteRune(ch)
write(ch)
beginningOfLine = false

View File

@ -475,6 +475,21 @@ Hope this helps.` + "`" + `
}`,
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
},
{
description: "imports before global options block keep standalone brace",
input: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
expect: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that

View File

@ -682,11 +682,28 @@ func (p *parser) directive() error {
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
}
return p.SyntaxErr("{")
}
return nil
}
func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
if p.Val() != "import" || len(p.block.Keys) == 0 {
return false
}
for _, key := range p.block.Keys {
if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
return false
}
}
return true
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not

View File

@ -930,6 +930,36 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
}
}
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
tempDir := t.TempDir()
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
if err != nil {
t.Fatalf("writing first import file: %v", err)
}
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
if err != nil {
t.Fatalf("writing second import file: %v", err)
}
_, err = Parse("Testfile", []byte(`import `+importFile1+`
import `+importFile2+`
{
debug
}`))
if err == nil {
t.Fatal("Expected an error, but got nil")
}
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
if !strings.HasPrefix(err.Error(), expected) {
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}