From cff6dea63ee88207b28170b1624adaa23e99a2b8 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Mon, 25 Mar 2024 18:06:52 +0900 Subject: [PATCH 01/22] acl formatting --- config/config.go | 13 +++++++++++++ go.mod | 1 + go.sum | 2 ++ token/token.go | 21 +++++++++++---------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index 0f87d387..e2cd45bb 100644 --- a/config/config.go +++ b/config/config.go @@ -47,6 +47,17 @@ type TestConfig struct { OverrideRequest *RequestConfig } +// Format configuration +type FormatConfig struct { + IndentWidth int `yaml:"indent_Width" default:"2"` + IndentStyle string `yaml:"indent_style" default:"space"` + BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` + ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` + FormatComment bool `yaml:"format_comment" default:"true"` + AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` + AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` +} + type Config struct { // Root configurations IncludePaths []string `cli:"I,include_path" yaml:"include_paths"` @@ -77,6 +88,8 @@ type Config struct { Simulator *SimulatorConfig `yaml:"simulator"` // Testing configuration Testing *TestConfig `yaml:"testing"` + // Format configuration + Format *FormatConfig `yaml:"format"` } func New(args []string) (*Config, error) { diff --git a/go.mod b/go.mod index c6b0ef79..ed7108fd 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect diff --git a/go.sum b/go.sum index 924b3d51..27ee6803 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/token/token.go b/token/token.go index 227c6af1..ef4a9e07 100644 --- a/token/token.go +++ b/token/token.go @@ -33,15 +33,16 @@ const ( EOF = "EOF" // Language idents - IDENT = "IDENT" - INT = "INT" - STRING = "STRING" - FLOAT = "FLOAT" - RTIME = "RTIME" - COMMENT = "COMMENT" - TRUE = "TRUE" - FALSE = "FALSE" - LF = "LF" // "\n" + IDENT = "IDENT" + INT = "INT" + STRING = "STRING" + FLOAT = "FLOAT" + RTIME = "RTIME" + COMMENT = "COMMENT" + TRUE = "TRUE" + FALSE = "FALSE" + LF = "LF" // "\n" + WHITESPACES = "WHITESPACES" // " ", "\r", "\t" // Operators // https://developer.fastly.com/reference/vcl/operators/ @@ -96,7 +97,7 @@ const ( DIRECTOR = "DIRECTOR" // director BACKEND = "BACKEND" // backend TABLE = "TABLE" // table - SUBROUTINE = "SUBROUTINE" // sub" + SUBROUTINE = "SUBROUTINE" // sub ADD = "ADD" // add CALL = "CALL" // call DECLARE = "DECLARE" // declare From 5aaf5b95800805b0f03b187dc89e1e2c9e02301a Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Tue, 26 Mar 2024 05:49:11 +0900 Subject: [PATCH 02/22] wip formatter --- config/config.go | 1 + formatter/comment_format.go | 21 ++ formatter/declaration_format.go | 217 ++++++++++++++ formatter/declaration_format_test.go | 410 +++++++++++++++++++++++++++ formatter/errors.go | 31 ++ formatter/expression_format.go | 220 ++++++++++++++ formatter/formatter.go | 90 ++++++ parser/expression_parser_test.go | 73 +++++ parser/parser.go | 13 +- 9 files changed, 1064 insertions(+), 12 deletions(-) create mode 100644 formatter/comment_format.go create mode 100644 formatter/declaration_format.go create mode 100644 formatter/declaration_format_test.go create mode 100644 formatter/errors.go create mode 100644 formatter/expression_format.go create mode 100644 formatter/formatter.go diff --git a/config/config.go b/config/config.go index e2cd45bb..cfa31bee 100644 --- a/config/config.go +++ b/config/config.go @@ -54,6 +54,7 @@ type FormatConfig struct { BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` FormatComment bool `yaml:"format_comment" default:"true"` + SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` } diff --git a/formatter/comment_format.go b/formatter/comment_format.go new file mode 100644 index 00000000..ee5282e4 --- /dev/null +++ b/formatter/comment_format.go @@ -0,0 +1,21 @@ +package formatter + +import ( + "bytes" + + "github.com/ysugimoto/falco/ast" +) + +func (f *Formatter) formatComment(comments ast.Comments, sep string, level int) string { + if len(comments) == 0 { + return "" + } + var buf bytes.Buffer + + for i := range comments { + buf.WriteString(f.indent(level) + comments[i].String()) + buf.WriteString(sep) + } + + return buf.String() +} diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go new file mode 100644 index 00000000..19f7dc33 --- /dev/null +++ b/formatter/declaration_format.go @@ -0,0 +1,217 @@ +package formatter + +import ( + "bytes" + "fmt" + "sort" + + "github.com/ysugimoto/falco/ast" +) + +func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("acl " + decl.Name.Value + " {\n") + for _, cidr := range decl.CIDRs { + buf.WriteString(f.formatComment(cidr.Leading, "\n", 1)) + buf.WriteString(f.indent(1)) + if cidr.Inverse != nil && cidr.Inverse.Value { + buf.WriteString("!") + if f.conf.AclInverseWithSpace { + buf.WriteString(" ") + } + } + buf.WriteString(`"` + cidr.IP.String() + `"`) + if cidr.Mask != nil { + buf.WriteString("/" + cidr.Mask.String()) + } + buf.WriteString(";") + if len(cidr.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(cidr.Trailing, "", 0)) + } + buf.WriteString("\n") + } + if len(decl.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("backend " + decl.Name.Value + " {\n") + buf.WriteString(f.formatBackendProperties(decl.Properties, 1)) + if len(decl.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLevel int) string { + var buf bytes.Buffer + var maxPropLength int + + if f.conf.SortDeclarationProperty { + sort.Slice(props, func(i, j int) bool { + if props[i].Key.String() == "probe" { + return false + } + return props[i].Key.String() < props[j].Key.String() + }) + } + + for i := range props { + if len(props[i].Key.String()) > maxPropLength { + maxPropLength = len(props[i].Key.String()) + } + } + + for _, prop := range props { + buf.WriteString(f.formatComment(prop.Leading, "\n", nestLevel)) + buf.WriteString(f.indent(nestLevel)) + if f.conf.AlignDeclarationProperty { + format := fmt.Sprintf("%%-%ds", maxPropLength) + buf.WriteString(fmt.Sprintf("."+format+" = ", prop.Key.String())) + } else { + buf.WriteString(fmt.Sprintf(".%s = ", prop.Key.String())) + } + if po, ok := prop.Value.(*ast.BackendProbeObject); ok { + buf.WriteString("{\n") + buf.WriteString(f.formatBackendProperties(po.Values, nestLevel+1)) + buf.WriteString(f.indent(nestLevel) + "}") + } else { + buf.WriteString(f.formatExpression(prop.Value)) + buf.WriteString(";") + } + if len(prop.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(prop.Trailing, "", 0)) + } + buf.WriteString("\n") + } + return buf.String() +} + +func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("director " + decl.Name.Value + " " + decl.DirectorType.Value + " {\n") + for _, prop := range decl.Properties { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatDirectorProperty(prop.(*ast.DirectorBackendObject))) + buf.WriteString("\n") + } + if len(decl.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatDirectorProperty(prop *ast.DirectorBackendObject) string { + var buf bytes.Buffer + + if f.conf.SortDeclarationProperty { + sort.Slice(prop.Values, func(i, j int) bool { + return prop.Values[i].Key.Value < prop.Values[j].Key.Value + }) + } + + buf.WriteString(f.formatComment(prop.Leading, "\n", 0)) + buf.WriteString("{ ") + for _, v := range prop.Values { + buf.WriteString(fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value))) + } + buf.WriteString("}") + if len(prop.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(prop.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("table " + decl.Name.Value) + if decl.ValueType != nil { + buf.WriteString(" " + decl.ValueType.Value) + } + buf.WriteString(" {\n") + buf.WriteString(f.formatTableProperties(decl.Properties)) + if len(decl.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { + var buf bytes.Buffer + var maxPropLength int + + if f.conf.SortDeclarationProperty { + sort.Slice(props, func(i, j int) bool { + return props[i].Key.Value < props[j].Key.Value + }) + } + + for i := range props { + if len(props[i].Key.String()) > maxPropLength { + maxPropLength = len(props[i].Key.String()) + } + } + + for _, prop := range props { + buf.WriteString(f.formatComment(prop.Leading, "\n", 0)) + buf.WriteString(f.indent(1)) + if f.conf.AlignDeclarationProperty { + format := fmt.Sprintf("%%-%ds", maxPropLength) + buf.WriteString(fmt.Sprintf(format+": ", f.formatString(prop.Key))) + } else { + buf.WriteString(fmt.Sprintf("%s: ", f.formatString(prop.Key))) + } + buf.WriteString(f.formatExpression(prop.Value)) + buf.WriteString(",") + if len(prop.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(prop.Trailing, "", 0)) + } + buf.WriteString("\n") + } + + return buf.String() +} diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go new file mode 100644 index 00000000..328a1867 --- /dev/null +++ b/formatter/declaration_format_test.go @@ -0,0 +1,410 @@ +package formatter + +import ( + "io/ioutil" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ysugimoto/falco/config" + "github.com/ysugimoto/falco/lexer" + "github.com/ysugimoto/falco/parser" +) + +func TestAclDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting", + input: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; + }`, + expect: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; +} +`, + }, + { + name: "with comment", + input: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; + // The ending + } // trailing`, + expect: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; + // The ending +} // trailing +`, + }, + { + name: "with inverse spacing", + input: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; + }`, + expect: `acl name { + "192.0.2.0"/24; // some comment + ! "192.0.2.12"; + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AclInverseWithSpace: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} + +func TestBackendDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting", + input: `backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } + }`, + expect: `backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } +} +`, + }, + { + name: "property alignment", + input: `backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } + }`, + expect: `backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AlignDeclarationProperty: true, + }, + }, + { + name: "sorted and alignment properties", + input: `backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } + }`, + expect: `backend example { + .between_bytes_timeout = 30s; + .connect_timeout = 1s; + .dynamic = true; + .first_byte_timeout = 30s; + .host = "example.com"; + .max_connections = 500; + .port = "443"; + .ssl = true; + .probe = { + .dummy = true; + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AlignDeclarationProperty: true, + SortDeclarationProperty: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} + +func TestDirectorDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting", + input: `director example hash { + { .backend=F_backend1; .weight=1; } + { .backend=F_backend2; .weight=1; } + { .backend=F_backend3; .weight=1; } + }`, + expect: `director example hash { + { .backend = F_backend1; .weight = 1; } + { .backend = F_backend2; .weight = 1; } + { .backend = F_backend3; .weight = 1; } +} +`, + }, + { + name: "sorted properties", + input: `director example hash { + { .weight=1; .backend=F_backend1; } + { .weight=1; .backend=F_backend2; } + { .weight=1; .backend=F_backend3; } + }`, + expect: `director example hash { + { .backend = F_backend1; .weight = 1; } + { .backend = F_backend2; .weight = 1; } + { .backend = F_backend3; .weight = 1; } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + SortDeclarationProperty: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} + +func TestTableDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting", + input: `table routing_table BACKEND { + "a.example.com":F_backendA, + "b.example.com":F_backendB, + "c.example.com":F_backendC, + }`, + expect: `table routing_table BACKEND { + "a.example.com": F_backendA, + "b.example.com": F_backendB, + "c.example.com": F_backendC, +} +`, + }, + { + name: "basic formatting without table type", + input: `table routing_table { + "a.example.com": "foo", + "b.example.com": "bar", + "c.example.com": "baz", + }`, + expect: `table routing_table { + "a.example.com": "foo", + "b.example.com": "bar", + "c.example.com": "baz", +} +`, + }, + { + name: "sorted properties", + input: `table routing_table BACKEND { + "c.example.com":F_backendC, + "b.example.com":F_backendB, + "a.example.com":F_backendA, + }`, + expect: `table routing_table BACKEND { + "a.example.com": F_backendA, + "b.example.com": F_backendB, + "c.example.com": F_backendC, +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + SortDeclarationProperty: true, + }, + }, + { + name: "alignment properties", + input: `table routing_table BACKEND { + "a.example.com":F_backendA, + "bb.example.com":F_backendB, + "ccc.example.com":F_backendC, + }`, + expect: `table routing_table BACKEND { + "a.example.com" : F_backendA, + "bb.example.com" : F_backendB, + "ccc.example.com": F_backendC, +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AlignDeclarationProperty: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} diff --git a/formatter/errors.go b/formatter/errors.go new file mode 100644 index 00000000..137b0f3d --- /dev/null +++ b/formatter/errors.go @@ -0,0 +1,31 @@ +package formatter + +import ( + "fmt" + + "github.com/ysugimoto/falco/token" +) + +type FormatError struct { + token token.Token + message string +} + +func (f *FormatError) Error() string { + return f.message +} + +// shorthand functions +func UnexpectedToken(actual token.Token, expects token.TokenType) *FormatError { + return &FormatError{ + token: actual, + message: fmt.Sprintf(`Unexpected token found. Expects "%s" but found "%s"`, expects, actual.Type), + } +} + +func UnexpectedEOF(actual token.Token) *FormatError { + return &FormatError{ + token: actual, + message: "Unexpected EOF", + } +} diff --git a/formatter/expression_format.go b/formatter/expression_format.go new file mode 100644 index 00000000..fbe44a42 --- /dev/null +++ b/formatter/expression_format.go @@ -0,0 +1,220 @@ +package formatter + +import ( + "fmt" + "strings" + + "github.com/ysugimoto/falco/ast" +) + +func (f *Formatter) formatExpression(expr ast.Expression) string { + switch t := expr.(type) { + case *ast.Ident: + return f.formatIdent(t) + case *ast.IP: + return f.formatIP(t) + case *ast.Boolean: + return f.formatBoolean(t) + case *ast.Integer: + return f.formatInteger(t) + case *ast.String: + return f.formatString(t) + case *ast.Float: + return f.formatFloat(t) + case *ast.RTime: + return f.formatRTime(t) + case *ast.PrefixExpression: + return f.formatPrefixExpression(t) + case *ast.GroupedExpression: + return f.formatGroupedExpression(t) + case *ast.InfixExpression: + return f.formatInfixExpression(t) + case *ast.IfExpression: + return f.formatIfExpression(t) + case *ast.FunctionCallExpression: + return f.formatFunctionCallExpression(t) + default: + return "" + } +} + +func (f *Formatter) formatIdent(expr *ast.Ident) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += expr.Value + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatIP(expr *ast.IP) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += expr.Value + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatBoolean(expr *ast.Boolean) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + str := fmt.Sprintf("%t", expr.Value) + if f.conf.BoolUpperCase { + ret += strings.ToUpper(str) + } else { + ret += strings.ToLower(str) + } + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatInteger(expr *ast.Integer) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += fmt.Sprint(expr.Value) + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatFloat(expr *ast.Float) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += fmt.Sprint(expr.Value) + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatString(expr *ast.String) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + if expr.Token.Offset == 4 { + // offset=4 means bracket-ed string line {"..."} + ret += fmt.Sprintf(`{"%s"}`, expr.Value) + } else { + // Otherwise, double quoted string + ret += fmt.Sprintf(`"%s"`, expr.Value) + } + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatRTime(expr *ast.RTime) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += expr.Value + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += expr.Operator + ret += f.formatExpression(expr.Right) + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += f.formatExpression(expr.Left) + if expr.Operator == "+" { // concatenation + if f.conf.ExplicitStringConat { + ret += " + " + } else { + ret += " " + } + } else { + ret += " " + expr.Operator + " " + } + ret += f.formatExpression(expr.Right) + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += "if(" + ret += f.formatExpression(expr.Condition) + ret += ", " + ret += f.formatExpression(expr.Consequence) + ret += ", " + ret += f.formatExpression(expr.Alternative) + ret += ")" + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += "(" + ret += f.formatExpression(expr.Right) + ret += ")" + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} + +func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) string { + var ret string + if v := f.formatComment(expr.Leading, "", 0); v != "" { + ret += v + " " + } + ret += expr.Function.Value + "(" + for i, arg := range expr.Arguments { + ret += f.formatExpression(arg) + if i != len(expr.Arguments)-1 { + ret += ", " + } + } + ret += ")" + if v := f.formatComment(expr.Trailing, "", 0); v != "" { + ret += " " + v + } + return ret +} diff --git a/formatter/formatter.go b/formatter/formatter.go new file mode 100644 index 00000000..dd800feb --- /dev/null +++ b/formatter/formatter.go @@ -0,0 +1,90 @@ +package formatter + +import ( + "bytes" + "io" + "strings" + + "github.com/ysugimoto/falco/ast" + "github.com/ysugimoto/falco/config" +) + +type Formatter struct { + conf *config.FormatConfig +} + +func New(conf *config.FormatConfig) *Formatter { + return &Formatter{ + conf: conf, + } +} + +func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { + buf := new(bytes.Buffer) + + var formatted string + for i := range vcl.Statements { + switch t := vcl.Statements[i].(type) { + case *ast.AclDeclaration: + formatted = f.formatAclDeclaration(t) + case *ast.BackendDeclaration: + formatted = f.formatBackendDeclaration(t) + case *ast.DirectorDeclaration: + formatted = f.formatDirectorDeclaration(t) + case *ast.TableDeclaration: + formatted = f.formatTableDeclaration(t) + } + buf.WriteString(formatted + "\n") + if i != len(vcl.Statements)-1 { + buf.WriteString("\n") + } + } + + return bytes.NewReader(buf.Bytes()), nil + + // for { + // tok := f.l.NextToken() + + // switch tok.Type { + // case token.ACL: + // formatted, err = f.formatAclDeclaration(conf) + // // case token.IMPORT: + // // formatted, err = f.formatImportStatement(conf) + // // case token.INCLUDE: + // // formatted, err = f.formatIncludeStatement(conf) + // case token.BACKEND: + // formatted, err = f.formatBackendDeclaration(conf) + // // case token.DIRECTOR: + // // formatted, err = f.formatDirectorDeclaration(conf) + // // case token.TABLE: + // // formatted, err = f.formatTableDeclaration(conf) + // // case token.SUBROUTINE: + // // formatted, err = f.formatSubroutineDeclaration(conf) + // // case token.PENALTYBOX: + // // formatted, err = f.formatPenaltyboxDeclaration(conf) + // // case token.RATECOUNTER: + // // formatted, err = f.formatRatecounterDeclaration(conf) + // // case token.COMMENT: + // // formatted, err = f.formatComment(conf, token) + // case token.ILLEGAL: + // return nil, errors.New("Invalid token found") + // case token.EOF: + // return bytes.NewReader(buf.Bytes()), nil + // default: + // // LF, WHITESPACES, ETC + // formatted = tok.Literal + // } + // if err != nil { + // return nil, errors.WithStack(err) + // } + // buf.WriteString(formatted) + // } +} + +func (f *Formatter) indent(level int) string { + c := " " // default as whitespace + if f.conf.IndentStyle == "tab" { + c = "\t" + } + return strings.Repeat(c, level*f.conf.IndentWidth) +} diff --git a/parser/expression_parser_test.go b/parser/expression_parser_test.go index 3bfe2562..407a630c 100644 --- a/parser/expression_parser_test.go +++ b/parser/expression_parser_test.go @@ -237,3 +237,76 @@ sub vcl_recv { assert(t, vcl, expect) }) } + +func TestInfixExpression(t *testing.T) { + t.Run("having infix comments in if condition", func(t *testing.T) { + input := ` +sub vcl_recv { + // foo + if (req.http.Foo && /* comment */ req.http.Bar) { + // foo + set req.http.Result = req.http.Foo req.http.Bar; + } + // end +}` + expect := &ast.VCL{ + Statements: []ast.Statement{ + &ast.SubroutineDeclaration{ + Meta: ast.New(T, 0), + Name: &ast.Ident{ + Meta: ast.New(T, 0), + Value: "vcl_recv", + }, + Block: &ast.BlockStatement{ + Meta: ast.New(T, 1), + Statements: []ast.Statement{ + &ast.IfStatement{ + Meta: ast.New(T, 1), + Condition: &ast.InfixExpression{ + Meta: ast.New(T, 1), + Operator: "&&", + Left: &ast.Ident{ + Meta: ast.New(T, 1), + Value: "req.http.Foo", + }, + Right: &ast.Ident{ + Meta: ast.New(T, 1), + Value: "req.http.Bar", + }, + }, + Consequence: &ast.BlockStatement{ + Statements: []ast.Statement{ + &ast.SetStatement{ + Meta: ast.New(T, 2), + Operator: &ast.Operator{ + Meta: ast.New(T, 2), + Operator: "=", + }, + Value: &ast.InfixExpression{ + Meta: ast.New(T, 2), + Operator: "", + Left: &ast.Ident{ + Meta: ast.New(T, 1), + Value: "req.http.Foo", + }, + Right: &ast.Ident{ + Meta: ast.New(T, 1), + Value: "req.http.Bar", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + vcl, err := New(lexer.NewFromString(input)).ParseVCL() + if err != nil { + t.Errorf("%+v", err) + } + assert(t, vcl, expect) + }) +} diff --git a/parser/parser.go b/parser/parser.go index cf733d9c..17a4db7e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1,8 +1,6 @@ package parser import ( - "strings" - "github.com/pkg/errors" "github.com/ysugimoto/falco/ast" "github.com/ysugimoto/falco/lexer" @@ -105,18 +103,9 @@ func (p *Parser) trailing() ast.Comments { for { // Analyze peek token tok := p.l.PeekToken() - if tok.Type == token.LF { + if tok.Type == token.LF || tok.Type == token.EOF { break } - if tok.Type == token.EOF { - if len(p.peekToken.LeadingComment()) > 0 { - cs = append(cs, &ast.Comment{ - Token: tok, - Value: strings.TrimSpace(p.peekToken.LeadingComment()), - }) - } - return cs - } if tok.Type == token.COMMENT { cs = append(cs, &ast.Comment{ Token: tok, From 005b85d031dd736e4a53d2df638696a68e84e4f0 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Tue, 26 Mar 2024 21:56:41 +0900 Subject: [PATCH 03/22] WIP statement and expression formatting --- formatter/declaration_format.go | 50 ++++ formatter/declaration_format_test.go | 142 +++++++++ formatter/formatter.go | 49 +--- formatter/statement_format.go | 413 +++++++++++++++++++++++++++ 4 files changed, 615 insertions(+), 39 deletions(-) create mode 100644 formatter/statement_format.go diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index 19f7dc33..4fb894bc 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -215,3 +215,53 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { return buf.String() } + +func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("penaltybox " + decl.Name.Value) + buf.WriteString(" {\n") + // penaltybox does not have properties + if len(decl.Block.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Block.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Block.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("ratecounter " + decl.Name.Value) + buf.WriteString(" {\n") + // ratecounter does not have properties + if len(decl.Block.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) + } + buf.WriteString("}") + if len(decl.Block.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(decl.Block.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) + buf.WriteString("sub " + decl.Name.Value) + buf.WriteString(f.formatBlockStatement(decl.Block)) + + return buf.String() +} diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 328a1867..3c128018 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -408,3 +408,145 @@ func TestTableDeclarationFormat(t *testing.T) { }) } } + +func TestPenaltyboxDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "formatting with comments", + input: `penaltybox banned_users { + # no properties + } // trailing comment`, + expect: `penaltybox banned_users { + # no properties +} // trailing comment +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} + +func TestRatecounterDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "formatting with comments", + input: `ratecounter requests_rate { + # no properties + } // trailing comment`, + expect: `ratecounter requests_rate { + # no properties +} // trailing comment +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} + +func TestSubroutineDeclarationFormat(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `// subroutine leading comment +sub vcl_recv { + set req.http.Foo = "bar"; + // subroutine infix comment +} // subroutine trailing comment`, + expect: `// subroutine leading comment +sub vcl_recv {{ + set req.http.Foo = "bar" ; + // subroutine infix comment +} // subroutine trailing comment +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + } + if tt.conf != nil { + c = tt.conf + } + vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), tt.expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + }) + } +} diff --git a/formatter/formatter.go b/formatter/formatter.go index dd800feb..5bec83da 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -25,6 +25,10 @@ func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { var formatted string for i := range vcl.Statements { switch t := vcl.Statements[i].(type) { + case *ast.ImportStatement: + formatted = f.formatImportStatement(t) + case *ast.IncludeStatement: + formatted = f.formatIncludeStatement(t) case *ast.AclDeclaration: formatted = f.formatAclDeclaration(t) case *ast.BackendDeclaration: @@ -33,52 +37,19 @@ func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { formatted = f.formatDirectorDeclaration(t) case *ast.TableDeclaration: formatted = f.formatTableDeclaration(t) + case *ast.PenaltyboxDeclaration: + formatted = f.formatPenaltyboxDeclaration(t) + case *ast.RatecounterDeclaration: + formatted = f.formatRatecounterDeclaration(t) + case *ast.SubroutineDeclaration: + formatted = f.formatSubroutineDeclaration(t) } buf.WriteString(formatted + "\n") if i != len(vcl.Statements)-1 { buf.WriteString("\n") } } - return bytes.NewReader(buf.Bytes()), nil - - // for { - // tok := f.l.NextToken() - - // switch tok.Type { - // case token.ACL: - // formatted, err = f.formatAclDeclaration(conf) - // // case token.IMPORT: - // // formatted, err = f.formatImportStatement(conf) - // // case token.INCLUDE: - // // formatted, err = f.formatIncludeStatement(conf) - // case token.BACKEND: - // formatted, err = f.formatBackendDeclaration(conf) - // // case token.DIRECTOR: - // // formatted, err = f.formatDirectorDeclaration(conf) - // // case token.TABLE: - // // formatted, err = f.formatTableDeclaration(conf) - // // case token.SUBROUTINE: - // // formatted, err = f.formatSubroutineDeclaration(conf) - // // case token.PENALTYBOX: - // // formatted, err = f.formatPenaltyboxDeclaration(conf) - // // case token.RATECOUNTER: - // // formatted, err = f.formatRatecounterDeclaration(conf) - // // case token.COMMENT: - // // formatted, err = f.formatComment(conf, token) - // case token.ILLEGAL: - // return nil, errors.New("Invalid token found") - // case token.EOF: - // return bytes.NewReader(buf.Bytes()), nil - // default: - // // LF, WHITESPACES, ETC - // formatted = tok.Literal - // } - // if err != nil { - // return nil, errors.WithStack(err) - // } - // buf.WriteString(formatted) - // } } func (f *Formatter) indent(level int) string { diff --git a/formatter/statement_format.go b/formatter/statement_format.go new file mode 100644 index 00000000..c014a428 --- /dev/null +++ b/formatter/statement_format.go @@ -0,0 +1,413 @@ +package formatter + +import ( + "bytes" + + "github.com/ysugimoto/falco/ast" +) + +func (f *Formatter) formatStatement(stmt ast.Statement) string { + switch t := stmt.(type) { + case *ast.BlockStatement: + return f.formatBlockStatement(t) + case *ast.ImportStatement: + return f.formatImportStatement(t) + case *ast.IncludeStatement: + return f.formatIncludeStatement(t) + case *ast.DeclareStatement: + return f.formatDeclareStatement(t) + case *ast.SetStatement: + return f.formatSetStatement(t) + case *ast.UnsetStatement: + return f.formatUnsetStatement(t) + case *ast.RemoveStatement: + return f.formatRemoveStatement(t) + case *ast.IfStatement: + return f.formatIfStatement(t) + case *ast.SwitchStatement: + return f.formatSwitchStatement(t) + case *ast.RestartStatement: + return f.formatRestartStatement(t) + case *ast.EsiStatement: + return f.formatEsiStatement(t) + case *ast.AddStatement: + return f.formatAddStatement(t) + case *ast.CallStatement: + return f.formatCallStatement(t) + case *ast.ErrorStatement: + return f.formatErrorStatement(t) + case *ast.LogStatement: + return f.formatLogStatement(t) + case *ast.ReturnStatement: + return f.formatReturnStatement(t) + case *ast.SyntheticStatement: + return f.formatSyntheticStatement(t) + case *ast.SyntheticBase64Statement: + return f.formatSyntheticBase64Statement(t) + case *ast.GotoStatement: + return f.formatGotoStatement(t) + case *ast.GotoDestinationStatement: + return f.formatGotoDestinationStatement(t) + case *ast.FunctionCallStatement: + return f.formatFunctionCallStatement(t) + } + return "" +} + +func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString("import ") + buf.WriteString(`"` + stmt.Name.Value + `"`) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString("include ") + buf.WriteString(f.formatString(stmt.Module)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString("{\n") + + for i := range stmt.Statements { + buf.WriteString(f.formatStatement(stmt.Statements[i])) + buf.WriteString("\n") + } + if len(stmt.Infix) > 0 { + buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) + } + buf.WriteString("}") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString("declare local " + stmt.Name.Value) + buf.WriteString(" " + stmt.ValueType.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString("set " + stmt.Ident.Value) + buf.WriteString(" " + stmt.Operator.Operator + " ") + buf.WriteString(f.formatExpression(stmt.Value)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("unset " + stmt.Ident.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("remove " + stmt.Ident.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { +} + +func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("switch (" + f.formatExpression(stmt.Control) + ") {\n") + for _, c := range stmt.Cases { + buf.WriteString(f.formatComment(c.Leading, "\n", c.Meta.Nest)) + buf.WriteString(f.indent(c.Meta.Nest)) + if c.Test != nil { + buf.WriteString("case ") + if c.Test.Operator == "~" { + buf.WriteString("~ ") + } + buf.WriteString(f.formatExpression(c.Test.Right)) + buf.WriteString(":\n") + } else { + buf.WriteString("default:\n") + } + for _, s := range c.Statements { + if _, ok := s.(*ast.BreakStatement); ok { + buf.WriteString(f.indent(c.Meta.Nest + 1)) + buf.WriteString("break;") + } else { + buf.WriteString(f.formatStatement(s)) + } + buf.WriteString("\n") + } + if c.Fallthrough { + buf.WriteString(f.indent(c.Meta.Nest + 1)) + buf.WriteString("fallthrough;\n") + } + } + if len(stmt.Infix) > 0 { + buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) + } + buf.WriteString("}") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatRestartStatement(stmt *ast.RestartStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("restart;") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatEsiStatement(stmt *ast.EsiStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("esi;") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("add " + stmt.Ident.Value) + buf.WriteString(" " + stmt.Operator.Operator + " ") + buf.WriteString(f.formatExpression(stmt.Value)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("call " + stmt.Subroutine.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("error " + f.formatExpression(stmt.Code)) + if stmt.Argument != nil { + buf.WriteString(" " + f.formatExpression(stmt.Argument)) + } + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("log " + f.formatExpression(stmt.Value)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("return") + if stmt.HasParenthesis { + buf.WriteString(" (") + } + if stmt.ReturnExpression != nil { + buf.WriteString(f.formatExpression(*stmt.ReturnExpression)) + } + if stmt.HasParenthesis { + buf.WriteString(")") + } + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("synthetic " + f.formatExpression(stmt.Value)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Statement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("synthetic.base64 " + f.formatExpression(stmt.Value)) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString("goto " + stmt.Destination.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} + +func (f *Formatter) formatGotoDestinationStatement(stmt *ast.GotoDestinationStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString(stmt.Name.Value) + buf.WriteString(";") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} +func (f *Formatter) formatFunctionCallStatement(stmt *ast.FunctionCallStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString(stmt.Function.Value + "(") + for i, a := range stmt.Arguments { + buf.WriteString(f.formatExpression(a)) + if i != len(stmt.Arguments)-1 { + buf.WriteString(", ") + } + } + buf.WriteString(");") + if len(stmt.Trailing) > 0 { + buf.WriteString(" ") + buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) + } + + return buf.String() +} From 6d94e0e9502db0738b915f78af8969b3ea11e191 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 27 Mar 2024 01:54:56 +0900 Subject: [PATCH 04/22] WIP statement formatter testing --- ast/if_statement.go | 5 +- ast/if_statement_test.go | 6 +- config/config.go | 19 +- formatter/declaration_format.go | 54 +- formatter/declaration_format_test.go | 176 +----- formatter/formatter.go | 11 + formatter/formatter_test.go | 37 ++ formatter/statement_format.go | 172 +++--- formatter/statement_format_test.go | 769 +++++++++++++++++++++++++++ parser/expression_parser_test.go | 19 +- parser/parser_test.go | 2 +- parser/statement_parser.go | 14 +- 12 files changed, 969 insertions(+), 315 deletions(-) create mode 100644 formatter/formatter_test.go create mode 100644 formatter/statement_format_test.go diff --git a/ast/if_statement.go b/ast/if_statement.go index a7d1330c..91fbe925 100644 --- a/ast/if_statement.go +++ b/ast/if_statement.go @@ -6,6 +6,7 @@ import ( type IfStatement struct { *Meta + Keyword string Condition Expression Consequence *BlockStatement Another []*IfStatement @@ -19,7 +20,7 @@ func (i *IfStatement) String() string { var buf bytes.Buffer buf.WriteString(i.LeadingComment()) - buf.WriteString(indent(i.Nest) + "if (") + buf.WriteString(indent(i.Nest) + i.Keyword + " (") buf.WriteString(i.Condition.String()) buf.WriteString(") ") buf.WriteString(i.Consequence.String()) @@ -27,7 +28,7 @@ func (i *IfStatement) String() string { for _, a := range i.Another { buf.WriteString("\n") buf.WriteString(a.LeadingComment()) - buf.WriteString(indent(i.Nest) + "else if (") + buf.WriteString(indent(i.Nest) + a.Keyword + " (") buf.WriteString(a.Condition.String()) buf.WriteString(") ") buf.WriteString(a.Consequence.String()) diff --git a/ast/if_statement_test.go b/ast/if_statement_test.go index 110cde44..47995c92 100644 --- a/ast/if_statement_test.go +++ b/ast/if_statement_test.go @@ -6,14 +6,16 @@ import ( func TestIfStatement(t *testing.T) { ifs := &IfStatement{ - Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")), + Keyword: "if", + Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")), Condition: &Ident{ Meta: New(T, 0), Value: "req.http.Host", }, Another: []*IfStatement{ { - Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")), + Keyword: "else if", + Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")), Condition: &Ident{ Meta: New(T, 0), Value: "req.http.Host", diff --git a/config/config.go b/config/config.go index cfa31bee..0f21dfce 100644 --- a/config/config.go +++ b/config/config.go @@ -49,14 +49,17 @@ type TestConfig struct { // Format configuration type FormatConfig struct { - IndentWidth int `yaml:"indent_Width" default:"2"` - IndentStyle string `yaml:"indent_style" default:"space"` - BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` - ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` - FormatComment bool `yaml:"format_comment" default:"true"` - SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` - AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` - AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` + IndentWidth int `yaml:"indent_Width" default:"2"` + TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` + IndentStyle string `yaml:"indent_style" default:"space"` + BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` + ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` + FormatComment bool `yaml:"format_comment" default:"true"` + SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` + AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` + AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` + ElseIf bool `yaml:"else_if" default:"false"` + ReturnArgumentParenthesis bool `yaml:"return_argument_parenthesis" default:"true"` } type Config struct { diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index 4fb894bc..e4ba09d1 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -27,10 +27,7 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { buf.WriteString("/" + cidr.Mask.String()) } buf.WriteString(";") - if len(cidr.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(cidr.Trailing, "", 0)) - } + buf.WriteString(f.trailing(cidr.Trailing)) buf.WriteString("\n") } if len(decl.Infix) > 0 { @@ -38,10 +35,7 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -57,10 +51,7 @@ func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) strin buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -101,10 +92,7 @@ func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLe buf.WriteString(f.formatExpression(prop.Value)) buf.WriteString(";") } - if len(prop.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(prop.Trailing, "", 0)) - } + buf.WriteString(f.trailing(prop.Trailing)) buf.WriteString("\n") } return buf.String() @@ -125,10 +113,7 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) str buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -148,10 +133,7 @@ func (f *Formatter) formatDirectorProperty(prop *ast.DirectorBackendObject) stri buf.WriteString(fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value))) } buf.WriteString("}") - if len(prop.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(prop.Trailing, "", 0)) - } + buf.WriteString(f.trailing(prop.Trailing)) return buf.String() } @@ -171,10 +153,7 @@ func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -206,10 +185,7 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { } buf.WriteString(f.formatExpression(prop.Value)) buf.WriteString(",") - if len(prop.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(prop.Trailing, "", 0)) - } + buf.WriteString(f.trailing(prop.Trailing)) buf.WriteString("\n") } @@ -228,10 +204,7 @@ func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Block.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Block.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Block.Trailing)) return buf.String() } @@ -248,10 +221,7 @@ func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaratio buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - if len(decl.Block.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(decl.Block.Trailing, "", 0)) - } + buf.WriteString(f.trailing(decl.Block.Trailing)) return buf.String() } @@ -260,8 +230,8 @@ func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) var buf bytes.Buffer buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) - buf.WriteString("sub " + decl.Name.Value) - buf.WriteString(f.formatBlockStatement(decl.Block)) + buf.WriteString("sub " + decl.Name.Value + " ") + buf.WriteString(f.formatBlockStatement(decl.Block, false)) return buf.String() } diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 3c128018..5bcdcd6e 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -1,13 +1,9 @@ package formatter import ( - "io/ioutil" "testing" - "github.com/google/go-cmp/cmp" "github.com/ysugimoto/falco/config" - "github.com/ysugimoto/falco/lexer" - "github.com/ysugimoto/falco/parser" ) func TestAclDeclarationFormat(t *testing.T) { @@ -61,36 +57,17 @@ func TestAclDeclarationFormat(t *testing.T) { } `, conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - AclInverseWithSpace: true, + IndentWidth: 2, + IndentStyle: "space", + AclInverseWithSpace: true, + TrailingCommentWidth: 2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -169,6 +146,7 @@ func TestBackendDeclarationFormat(t *testing.T) { IndentWidth: 2, IndentStyle: "space", AlignDeclarationProperty: true, + TrailingCommentWidth: 2, }, }, { @@ -207,33 +185,14 @@ func TestBackendDeclarationFormat(t *testing.T) { IndentStyle: "space", AlignDeclarationProperty: true, SortDeclarationProperty: true, + TrailingCommentWidth: 2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -276,33 +235,14 @@ func TestDirectorDeclarationFormat(t *testing.T) { IndentWidth: 2, IndentStyle: "space", SortDeclarationProperty: true, + TrailingCommentWidth: 2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -359,6 +299,7 @@ func TestTableDeclarationFormat(t *testing.T) { IndentWidth: 2, IndentStyle: "space", SortDeclarationProperty: true, + TrailingCommentWidth: 2, }, }, { @@ -378,33 +319,14 @@ func TestTableDeclarationFormat(t *testing.T) { IndentWidth: 2, IndentStyle: "space", AlignDeclarationProperty: true, + TrailingCommentWidth: 2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -430,27 +352,7 @@ func TestPenaltyboxDeclarationFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -476,27 +378,7 @@ func TestRatecounterDeclarationFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } @@ -516,9 +398,9 @@ sub vcl_recv { // subroutine infix comment } // subroutine trailing comment`, expect: `// subroutine leading comment -sub vcl_recv {{ - set req.http.Foo = "bar" ; - // subroutine infix comment +sub vcl_recv { + set req.http.Foo = "bar"; + // subroutine infix comment } // subroutine trailing comment `, }, @@ -526,27 +408,7 @@ sub vcl_recv {{ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - } - if tt.conf != nil { - c = tt.conf - } - vcl, err := parser.New(lexer.NewFromString(tt.input)).ParseVCL() - if err != nil { - t.Errorf("Unexpected parser error: %s", err) - return - } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return - } - v, _ := ioutil.ReadAll(ret) - if diff := cmp.Diff(string(v), tt.expect); diff != "" { - t.Errorf("Format result has diff: %s", diff) - } + assert(t, tt.input, tt.expect, tt.conf) }) } } diff --git a/formatter/formatter.go b/formatter/formatter.go index 5bec83da..801f4df1 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -59,3 +59,14 @@ func (f *Formatter) indent(level int) string { } return strings.Repeat(c, level*f.conf.IndentWidth) } + +func (f *Formatter) trailing(trailing ast.Comments) string { + var c string + + if len(trailing) == 0 { + return c + } + c += strings.Repeat(" ", f.conf.TrailingCommentWidth) + c += f.formatComment(trailing, "", 0) + return c +} diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go new file mode 100644 index 00000000..5c8e808f --- /dev/null +++ b/formatter/formatter_test.go @@ -0,0 +1,37 @@ +package formatter + +import ( + "io/ioutil" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ysugimoto/falco/config" + "github.com/ysugimoto/falco/lexer" + "github.com/ysugimoto/falco/parser" +) + +func assert(t *testing.T, input, expect string, conf *config.FormatConfig) string { + c := &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + } + if conf != nil { + c = conf + } + vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL() + if err != nil { + t.Errorf("Unexpected parser error: %s", err) + return "" + } + ret, err := New(c).Format(vcl) + if err != nil { + t.Errorf("Unexpected error returned: %s", err) + return "" + } + v, _ := ioutil.ReadAll(ret) + if diff := cmp.Diff(string(v), expect); diff != "" { + t.Errorf("Format result has diff: %s", diff) + } + return string(v) // return formatted result for debugging +} diff --git a/formatter/statement_format.go b/formatter/statement_format.go index c014a428..5046bbec 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -9,7 +9,7 @@ import ( func (f *Formatter) formatStatement(stmt ast.Statement) string { switch t := stmt.(type) { case *ast.BlockStatement: - return f.formatBlockStatement(t) + return f.formatBlockStatement(t, true) case *ast.ImportStatement: return f.formatImportStatement(t) case *ast.IncludeStatement: @@ -58,13 +58,11 @@ func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { var buf bytes.Buffer buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("import ") - buf.WriteString(`"` + stmt.Name.Value + `"`) + buf.WriteString(stmt.Name.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -73,21 +71,23 @@ func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { var buf bytes.Buffer buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("include ") buf.WriteString(f.formatString(stmt.Module)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } -func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { +func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement, isIndependent bool) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + if isIndependent { + // need subtract 1 because LEFT_BRACE is unnested + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest-1)) + buf.WriteString(f.indent(stmt.Meta.Nest - 1)) + } buf.WriteString("{\n") for i := range stmt.Statements { @@ -97,11 +97,10 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { if len(stmt.Infix) > 0 { buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) } + // need subtract 1 because RIGHT_BRACE is unnested + buf.WriteString(f.indent(stmt.Meta.Nest - 1)) buf.WriteString("}") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -110,13 +109,11 @@ func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { var buf bytes.Buffer buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("declare local " + stmt.Name.Value) buf.WriteString(" " + stmt.ValueType.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -125,14 +122,12 @@ func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { var buf bytes.Buffer buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("set " + stmt.Ident.Value) buf.WriteString(" " + stmt.Operator.Operator + " ") buf.WriteString(f.formatExpression(stmt.Value)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -144,10 +139,7 @@ func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("unset " + stmt.Ident.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -159,15 +151,50 @@ func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("remove " + stmt.Ident.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { + var buf bytes.Buffer + + buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) + buf.WriteString(f.indent(stmt.Meta.Nest)) + buf.WriteString(stmt.Keyword + " (" + f.formatExpression(stmt.Condition) + ") ") + buf.WriteString(f.formatBlockStatement(stmt.Consequence, false)) + for _, a := range stmt.Another { + // If leading comments exists, keyword should be placed with line-feed + if len(a.Leading) > 0 { + buf.WriteString("\n") + buf.WriteString(f.formatComment(a.Leading, "\n", a.Nest)) + buf.WriteString(f.indent(a.Nest)) + } else { + // Otherwise, write one whitespace characeter + buf.WriteString(" ") + } + + keyword := a.Keyword + if f.conf.ElseIf { + keyword = "else if" + } + buf.WriteString(keyword + " (" + f.formatExpression(a.Condition) + ") ") + buf.WriteString(f.formatBlockStatement(a.Consequence, false)) + } + if stmt.Alternative != nil { + if len(stmt.Alternative.Leading) > 0 { + buf.WriteString("\n") + buf.WriteString(f.formatComment(stmt.Alternative.Leading, "\n", stmt.Alternative.Nest)) + buf.WriteString(f.indent(stmt.Alternative.Nest)) + } else { + buf.WriteString(" ") + } + buf.WriteString("else ") + buf.WriteString(f.formatBlockStatement(stmt.Alternative, false)) + } + buf.WriteString(f.trailing(stmt.Trailing)) + + return buf.String() } func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { @@ -206,11 +233,9 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { if len(stmt.Infix) > 0 { buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) } + buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("}") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -221,10 +246,7 @@ func (f *Formatter) formatRestartStatement(stmt *ast.RestartStatement) string { buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("restart;") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -235,10 +257,7 @@ func (f *Formatter) formatEsiStatement(stmt *ast.EsiStatement) string { buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("esi;") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -252,10 +271,7 @@ func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { buf.WriteString(" " + stmt.Operator.Operator + " ") buf.WriteString(f.formatExpression(stmt.Value)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -267,10 +283,7 @@ func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("call " + stmt.Subroutine.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -285,10 +298,7 @@ func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { buf.WriteString(" " + f.formatExpression(stmt.Argument)) } buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -300,10 +310,7 @@ func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("log " + f.formatExpression(stmt.Value)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -314,20 +321,19 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("return") - if stmt.HasParenthesis { - buf.WriteString(" (") - } if stmt.ReturnExpression != nil { + prefix := " " + suffix := "" + if f.conf.ReturnArgumentParenthesis { + prefix = " (" + suffix = ")" + } + buf.WriteString(prefix) buf.WriteString(f.formatExpression(*stmt.ReturnExpression)) - } - if stmt.HasParenthesis { - buf.WriteString(")") + buf.WriteString(suffix) } buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -339,10 +345,7 @@ func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) strin buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("synthetic " + f.formatExpression(stmt.Value)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -354,10 +357,7 @@ func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Stat buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("synthetic.base64 " + f.formatExpression(stmt.Value)) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -369,10 +369,7 @@ func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("goto " + stmt.Destination.Value) buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -383,11 +380,7 @@ func (f *Formatter) formatGotoDestinationStatement(stmt *ast.GotoDestinationStat buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString(stmt.Name.Value) - buf.WriteString(";") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -404,10 +397,7 @@ func (f *Formatter) formatFunctionCallStatement(stmt *ast.FunctionCallStatement) } } buf.WriteString(");") - if len(stmt.Trailing) > 0 { - buf.WriteString(" ") - buf.WriteString(f.formatComment(stmt.Trailing, "", 0)) - } + buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go new file mode 100644 index 00000000..07470c9f --- /dev/null +++ b/formatter/statement_format_test.go @@ -0,0 +1,769 @@ +package formatter + +import ( + "testing" + + "github.com/ysugimoto/falco/config" +) + +func TestFormatImportStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `// import leading comment + import boltsort; // import trailing comment +`, + expect: `// import leading comment +import boltsort; // import trailing comment +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatIncludeStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `// include leading comment + include "feature_mod"; // include trailing comment +`, + expect: `// include leading comment +include "feature_mod"; // include trailing comment +`, + }, + { + name: "basic formatting without semicolon", + input: `// include leading comment + include "feature_mod" // include trailing comment +`, + expect: `// include leading comment +include "feature_mod"; // include trailing comment +`, + }, + { + name: "inside subroutine", + input: `sub vcl_recv { + // include leading comment + include "feature_mod" // include trailing comment +} +`, + expect: `sub vcl_recv { + // include leading comment + include "feature_mod"; // include trailing comment +} +`, + }, + { + name: "inside if condition block", + input: `sub vcl_recv { + if (req.http.Host) { + // include leading comment + include "feature_mod" // include trailing comment + } +} +`, + expect: `sub vcl_recv { + if (req.http.Host) { + // include leading comment + include "feature_mod"; // include trailing comment + } +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatBlockStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // block leading comment + { + restart; + // block infix comment + } // block trailing comment +} +`, + expect: `sub vcl_recv { + // block leading comment + { + restart; + // block infix comment + } // block trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatDeclareStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // declare leading comment + declare local var.FOO STRING ; // declare trailing comment +} +`, + expect: `sub vcl_recv { + // declare leading comment + declare local var.FOO STRING; // declare trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatSetStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // set leading comment + set req.http.Foo= "foo" "bar" "baz" ; // set trailing comment +} +`, + expect: `sub vcl_recv { + // set leading comment + set req.http.Foo = "foo" "bar" "baz"; // set trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatUnsetStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // unset leading comment + unset req.http.Foo ; // unset trailing comment +} +`, + expect: `sub vcl_recv { + // unset leading comment + unset req.http.Foo; // unset trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatRemoveStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // remove leading comment + remove req.http.Foo ; // remove trailing comment +} +`, + expect: `sub vcl_recv { + // remove leading comment + remove req.http.Foo; // remove trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatRestartStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // restart leading comment + restart ; // restart trailing comment +} +`, + expect: `sub vcl_recv { + // restart leading comment + restart; // restart trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatEsitStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // esi leading comment + // esi leading comment + esi ; // esi trailing comment +} +`, + expect: `sub vcl_recv { + // esi leading comment + // esi leading comment + esi; // esi trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatAddStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_deliver { + // add leading comment + add resp.http.Set-Cookie= "name=foo" ; // add trailing comment +} +`, + expect: `sub vcl_deliver { + // add leading comment + add resp.http.Set-Cookie = "name=foo"; // add trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatCallStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // call leading comment + call feature_recv ; // call trailing comment +} +`, + expect: `sub vcl_recv { + // call leading comment + call feature_recv; // call trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatErrorStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // error leading comment + error 404 ; // error trailing comment +} +`, + expect: `sub vcl_recv { + // error leading comment + error 404; // error trailing comment +} +`, + }, + { + name: "formatting with response", + input: `sub vcl_recv { + // error leading comment + error 404 "extra response" ; // error trailing comment +} +`, + expect: `sub vcl_recv { + // error leading comment + error 404 "extra response"; // error trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatLogStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_log { + // log leading comment + log req.http.Host ; // log trailing comment +} +`, + expect: `sub vcl_log { + // log leading comment + log req.http.Host; // log trailing comment +} +`, + }, + { + name: "multiple expressions", + input: `sub vcl_log { + log req.http.Host "foo" "bar" "baz" client.ip ; + +} +`, + expect: `sub vcl_log { + log req.http.Host "foo" "bar" "baz" client.ip; +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatReturnStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // return leading comment + return lookup ; // return trailing comment +} +`, + expect: `sub vcl_recv { + // return leading comment + return lookup; // return trailing comment +} +`, + }, + { + name: "with parenthesis", + input: `sub vcl_recv { + return(lookup) ; +} +`, + expect: `sub vcl_recv { + return (lookup); +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + ReturnArgumentParenthesis: true, + }, + }, + { + name: "without argument", + input: `sub vcl_recv { + return ; +} +`, + expect: `sub vcl_recv { + return; +} +`, + }, + { + name: "unwrap parenthesis from configuration", + input: `sub vcl_recv { + return(lookup) ; +} +`, + expect: `sub vcl_recv { + return lookup; +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + ReturnArgumentParenthesis: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatSynthticStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_error { + // synthetic leading comment + synthetic "foo" ; // synthetic trailing comment +} +`, + expect: `sub vcl_error { + // synthetic leading comment + synthetic "foo"; // synthetic trailing comment +} +`, + }, + { + name: "with bracket string", + input: `sub vcl_error { + synthetic {"foo bar baz"} ; +} +`, + expect: `sub vcl_error { + synthetic {"foo bar baz"}; +} +`, + }, + { + name: "with multipel expressions", + input: `sub vcl_error { + synthetic {"foo bar baz"} "lorem" "ipsum" req.http.Hoost ; +} +`, + expect: `sub vcl_error { + synthetic {"foo bar baz"} "lorem" "ipsum" req.http.Hoost; +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatSynthticBase64Statement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_error { + // synthetic.base64 leading comment + synthetic.base64 "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo=" ; // synthetic.base64 trailing comment +} +`, + expect: `sub vcl_error { + // synthetic.base64 leading comment + synthetic.base64 "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo="; // synthetic.base64 trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatGotoStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // goto leading comment + goto FOO ; // goto trailing comment +} +`, + expect: `sub vcl_recv { + // goto leading comment + goto FOO; // goto trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatGotoDestinationStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + goto FOO; + // goto destination leading comment + FOO: // goto destination trailing comment +} +`, + expect: `sub vcl_recv { + goto FOO; + // goto destination leading comment + FOO: // goto destination trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatFunctionCallStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // function call leading comment + h3.alt_svc(); // function call trailing comment +} +`, + expect: `sub vcl_recv { + // function call leading comment + h3.alt_svc(); // function call trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestFormatIfStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // if leading comment + if (req.http.Host) { + set req.http.Foo = req.http.Host; + // if infix comment + } else { + set req.http.Foo = "unknown"; + // else infix comment + } // if trailing comment +} +`, + expect: `sub vcl_recv { + // if leading comment + if (req.http.Host) { + set req.http.Foo = req.http.Host; + // if infix comment + } else { + set req.http.Foo = "unknown"; + // else infix comment + } // if trailing comment +} +`, + }, + { + name: "multiple else if, line-feeding", + input: `sub vcl_recv { + // if leading comment + if (req.http.Host) { + set req.http.Foo = req.http.Host; + // if infix comment + } else if (req.http.AnotherHost) { + set req.http.Foo = "another"; + } + // More complecated case + else if (req.http.Other) { + set req.http.Foo = "other"; + } else { + set req.http.Foo = "unknown"; + // else infix comment + } // if trailing comment +} +`, + expect: `sub vcl_recv { + // if leading comment + if (req.http.Host) { + set req.http.Foo = req.http.Host; + // if infix comment + } else if (req.http.AnotherHost) { + set req.http.Foo = "another"; + } + // More complecated case + else if (req.http.Other) { + set req.http.Foo = "other"; + } else { + set req.http.Foo = "unknown"; + // else infix comment + } // if trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} diff --git a/parser/expression_parser_test.go b/parser/expression_parser_test.go index 407a630c..8fba17d4 100644 --- a/parser/expression_parser_test.go +++ b/parser/expression_parser_test.go @@ -258,10 +258,10 @@ sub vcl_recv { Value: "vcl_recv", }, Block: &ast.BlockStatement{ - Meta: ast.New(T, 1), + Meta: ast.New(T, 1, ast.Comments{}, ast.Comments{}, comments("// end")), Statements: []ast.Statement{ &ast.IfStatement{ - Meta: ast.New(T, 1), + Meta: ast.New(T, 1, comments("// foo")), Condition: &ast.InfixExpression{ Meta: ast.New(T, 1), Operator: "&&", @@ -270,27 +270,32 @@ sub vcl_recv { Value: "req.http.Foo", }, Right: &ast.Ident{ - Meta: ast.New(T, 1), + Meta: ast.New(T, 1, comments("/* comment */")), Value: "req.http.Bar", }, }, Consequence: &ast.BlockStatement{ + Meta: ast.New(T, 2), Statements: []ast.Statement{ &ast.SetStatement{ - Meta: ast.New(T, 2), + Meta: ast.New(T, 2, comments("// foo")), Operator: &ast.Operator{ Meta: ast.New(T, 2), Operator: "=", }, + Ident: &ast.Ident{ + Meta: ast.New(T, 2), + Value: "req.http.Result", + }, Value: &ast.InfixExpression{ Meta: ast.New(T, 2), - Operator: "", + Operator: "+", Left: &ast.Ident{ - Meta: ast.New(T, 1), + Meta: ast.New(T, 2), Value: "req.http.Foo", }, Right: &ast.Ident{ - Meta: ast.New(T, 1), + Meta: ast.New(T, 2), Value: "req.http.Bar", }, }, diff --git a/parser/parser_test.go b/parser/parser_test.go index 612baf83..7148899f 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -48,7 +48,7 @@ func assert(t *testing.T, actual, expect interface{}) { cmpopts.IgnoreFields(ast.InfixExpression{}), cmpopts.IgnoreFields(ast.PrefixExpression{}), cmpopts.IgnoreFields(ast.GroupedExpression{}), - cmpopts.IgnoreFields(ast.IfStatement{}, "AlternativeComments"), + cmpopts.IgnoreFields(ast.IfStatement{}, "AlternativeComments", "Keyword"), cmpopts.IgnoreFields(ast.UnsetStatement{}), cmpopts.IgnoreFields(ast.AddStatement{}), cmpopts.IgnoreFields(ast.CallStatement{}), diff --git a/parser/statement_parser.go b/parser/statement_parser.go index 2aa08a2e..8cd0a18f 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -509,7 +509,8 @@ func (p *Parser) parseSyntheticBase64Statement() (*ast.SyntheticBase64Statement, func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { stmt := &ast.IfStatement{ - Meta: p.curToken, + Keyword: "if", + Meta: p.curToken, } if !p.expectPeek(token.LEFT_PAREN) { @@ -546,11 +547,13 @@ func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { // If more peek token is IF, it should be "else if" if p.peekTokenIs(token.IF) { // else if + leading := p.curToken.Leading p.nextToken() // point to IF - another, err := p.parseAnotherIfStatement() + another, err := p.parseAnotherIfStatement("else if") if err != nil { return nil, errors.WithStack(err) } + another.Leading = leading stmt.Another = append(stmt.Another, another) continue } @@ -568,7 +571,7 @@ func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { // Note: VCL could define "else if" statement with "elseif", "elsif" keyword case token.ELSEIF, token.ELSIF: // elseif, elsif p.nextToken() // point to ELSEIF/ELSIF - another, err := p.parseAnotherIfStatement() + another, err := p.parseAnotherIfStatement(p.peekToken.Token.Literal) if err != nil { return nil, errors.WithStack(err) } @@ -584,9 +587,10 @@ FINISH: } // AnotherIfStatement is similar to IfStatement but is not culious about alternative. -func (p *Parser) parseAnotherIfStatement() (*ast.IfStatement, error) { +func (p *Parser) parseAnotherIfStatement(keyword string) (*ast.IfStatement, error) { stmt := &ast.IfStatement{ - Meta: p.curToken, + Keyword: keyword, + Meta: p.curToken, } if !p.expectPeek(token.LEFT_PAREN) { From 8ef231f023bbdcdf87244881d8912c6482da6500 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 27 Mar 2024 02:18:46 +0900 Subject: [PATCH 05/22] finished statement parging --- config/config.go | 22 ++-- config/config_test.go | 12 +++ formatter/expression_format.go | 182 ++++++++++++++++++--------------- 3 files changed, 125 insertions(+), 91 deletions(-) diff --git a/config/config.go b/config/config.go index 0f21dfce..77b8b192 100644 --- a/config/config.go +++ b/config/config.go @@ -49,17 +49,17 @@ type TestConfig struct { // Format configuration type FormatConfig struct { - IndentWidth int `yaml:"indent_Width" default:"2"` - TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` - IndentStyle string `yaml:"indent_style" default:"space"` - BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` - ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` - FormatComment bool `yaml:"format_comment" default:"true"` - SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` - AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` - AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` - ElseIf bool `yaml:"else_if" default:"false"` - ReturnArgumentParenthesis bool `yaml:"return_argument_parenthesis" default:"true"` + IndentWidth int `yaml:"indent_Width" default:"2"` + TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` + IndentStyle string `yaml:"indent_style" default:"space"` + BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` + ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` + // FormatComment bool `yaml:"format_comment" default:"true"` + SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` + AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` + AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` + ElseIf bool `yaml:"else_if" default:"false"` + ReturnArgumentParenthesis bool `yaml:"return_argument_parenthesis" default:"true"` } type Config struct { diff --git a/config/config_test.go b/config/config_test.go index d4628425..c2353ba9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -63,6 +63,18 @@ func TestConfigFromCLI(t *testing.T) { IncludePaths: []string{"."}, OverrideRequest: &RequestConfig{}, }, + Format: &FormatConfig{ + IndentWidth: 2, + TrailingCommentWidth: 2, + IndentStyle: "space", + BoolUpperCase: false, + ExplicitStringConat: false, + SortDeclarationProperty: false, + AlignDeclarationProperty: false, + AclInverseWithSpace: false, + ElseIf: false, + ReturnArgumentParenthesis: true, + }, OverrideBackends: make(map[string]*OverrideBackend), } diff --git a/formatter/expression_format.go b/formatter/expression_format.go index fbe44a42..b238d0b7 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -1,6 +1,7 @@ package formatter import ( + "bytes" "fmt" "strings" @@ -39,182 +40,203 @@ func (f *Formatter) formatExpression(expr ast.Expression) string { } func (f *Formatter) formatIdent(expr *ast.Ident) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += expr.Value + buf.WriteString(expr.Value) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + return buf.String() } func (f *Formatter) formatIP(expr *ast.IP) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += expr.Value + buf.WriteString(expr.Value) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + return buf.String() } func (f *Formatter) formatBoolean(expr *ast.Boolean) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } str := fmt.Sprintf("%t", expr.Value) if f.conf.BoolUpperCase { - ret += strings.ToUpper(str) + buf.WriteString(strings.ToUpper(str)) } else { - ret += strings.ToLower(str) + buf.WriteString(strings.ToLower(str)) } if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatInteger(expr *ast.Integer) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += fmt.Sprint(expr.Value) + buf.WriteString(fmt.Sprint(expr.Value)) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatFloat(expr *ast.Float) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += fmt.Sprint(expr.Value) + buf.WriteString(fmt.Sprint(expr.Value)) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatString(expr *ast.String) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } if expr.Token.Offset == 4 { - // offset=4 means bracket-ed string line {"..."} - ret += fmt.Sprintf(`{"%s"}`, expr.Value) + // offset=4 means bracket string like {"..."} + buf.WriteString(fmt.Sprintf(`{"%s"}`, expr.Value)) } else { // Otherwise, double quoted string - ret += fmt.Sprintf(`"%s"`, expr.Value) + buf.WriteString(fmt.Sprintf(`"%s"`, expr.Value)) } if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatRTime(expr *ast.RTime) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += expr.Value + buf.WriteString(expr.Value) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += expr.Operator - ret += f.formatExpression(expr.Right) + buf.WriteString(expr.Operator) + buf.WriteString(f.formatExpression(expr.Right)) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += f.formatExpression(expr.Left) + buf.WriteString(f.formatExpression(expr.Left)) + if expr.Operator == "+" { // concatenation if f.conf.ExplicitStringConat { - ret += " + " + buf.WriteString(" + ") } else { - ret += " " + buf.WriteString(" ") } } else { - ret += " " + expr.Operator + " " + buf.WriteString(" " + expr.Operator + " ") } - ret += f.formatExpression(expr.Right) + buf.WriteString(f.formatExpression(expr.Right)) if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " - } - ret += "if(" - ret += f.formatExpression(expr.Condition) - ret += ", " - ret += f.formatExpression(expr.Consequence) - ret += ", " - ret += f.formatExpression(expr.Alternative) - ret += ")" + buf.WriteString(v + " ") + } + buf.WriteString("if(") + buf.WriteString(f.formatExpression(expr.Condition)) + buf.WriteString(", ") + buf.WriteString(f.formatExpression(expr.Consequence)) + buf.WriteString(", ") + buf.WriteString(f.formatExpression(expr.Alternative)) + buf.WriteString(")") if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += "(" - ret += f.formatExpression(expr.Right) - ret += ")" + buf.WriteString("(" + f.formatExpression(expr.Right) + ")") if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(" " + v) } - return ret + + return buf.String() } func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) string { - var ret string + var buf bytes.Buffer + if v := f.formatComment(expr.Leading, "", 0); v != "" { - ret += v + " " + buf.WriteString(v + " ") } - ret += expr.Function.Value + "(" + buf.WriteString(expr.Function.Value + "(") for i, arg := range expr.Arguments { - ret += f.formatExpression(arg) + buf.WriteString(f.formatExpression(arg)) if i != len(expr.Arguments)-1 { - ret += ", " + buf.WriteString(", ") } } - ret += ")" + buf.WriteString(")") if v := f.formatComment(expr.Trailing, "", 0); v != "" { - ret += " " + v + buf.WriteString(v + " ") } - return ret + + return buf.String() } From 225185eb79af78d1c2a15ec295af7753ce101238 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 27 Mar 2024 20:08:09 +0900 Subject: [PATCH 06/22] finished basic implementation --- config/config.go | 6 + config/config_test.go | 1 + formatter/chunk_buffer.go | 71 ++++++++ formatter/chunk_buffer_test.go | 122 ++++++++++++++ formatter/declaration_format.go | 25 +-- formatter/declaration_format_test.go | 6 + formatter/expression_format.go | 232 +++++++++------------------ formatter/formatter.go | 25 ++- formatter/formatter_test.go | 1 + formatter/statement_format.go | 203 +++++++++++------------ formatter/statement_format_test.go | 7 + 11 files changed, 410 insertions(+), 289 deletions(-) create mode 100644 formatter/chunk_buffer.go create mode 100644 formatter/chunk_buffer_test.go diff --git a/config/config.go b/config/config.go index 77b8b192..3fe439ec 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,11 @@ var ( configurationFiles = []string{".falco.yaml", ".falco.yml"} ) +const ( + IndentStyleSpace = "space" + IndentStyleTab = "tab" +) + type OverrideBackend struct { Host string `yaml:"host"` SSL bool `yaml:"ssl" default:"true"` @@ -52,6 +57,7 @@ type FormatConfig struct { IndentWidth int `yaml:"indent_Width" default:"2"` TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` IndentStyle string `yaml:"indent_style" default:"space"` + LineWidth int `yaml:"line_width" default:"80"` BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` // FormatComment bool `yaml:"format_comment" default:"true"` diff --git a/config/config_test.go b/config/config_test.go index c2353ba9..80e89531 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -66,6 +66,7 @@ func TestConfigFromCLI(t *testing.T) { Format: &FormatConfig{ IndentWidth: 2, TrailingCommentWidth: 2, + LineWidth: 80, IndentStyle: "space", BoolUpperCase: false, ExplicitStringConat: false, diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go new file mode 100644 index 00000000..0d471f80 --- /dev/null +++ b/formatter/chunk_buffer.go @@ -0,0 +1,71 @@ +package formatter + +import ( + "bytes" + "strings" + + "github.com/ysugimoto/falco/config" +) + +type ChunkBuffer struct { + chunks []string + conf *config.FormatConfig +} + +func newBuffer(c *config.FormatConfig) *ChunkBuffer { + return &ChunkBuffer{ + chunks: []string{}, + conf: c, + } +} + +func (c *ChunkBuffer) Merge(nc *ChunkBuffer) { + c.chunks = append(c.chunks, nc.chunks...) +} + +func (c *ChunkBuffer) WriteString(s string) { + c.chunks = append(c.chunks, s) +} + +func (c *ChunkBuffer) String() string { + return strings.Join(c.chunks, "") +} + +func (c *ChunkBuffer) ChunkedString(level, offset int) string { + var buf bytes.Buffer + + count := offset + level*c.conf.IndentWidth + for i, b := range c.chunks { + if count+len(b) > c.conf.LineWidth { + buf.WriteString("\n") + buf.WriteString(c.indent(level)) + if offset > 0 { + buf.WriteString(c.offsetString(offset)) + } + count = offset + level*c.conf.IndentWidth + } else if i != 0 { + buf.WriteString(" ") + count++ + } + buf.WriteString(b) + count += len(b) + } + + return buf.String() +} + +func (c *ChunkBuffer) indent(level int) string { + ws := " " // default as whitespace + if c.conf.IndentStyle == config.IndentStyleTab { + ws = "\t" + } + return strings.Repeat(ws, level*c.conf.IndentWidth) +} + +func (c *ChunkBuffer) offsetString(offset int) string { + ws := " " // default as whitespace + if c.conf.IndentStyle == config.IndentStyleTab { + ws = "\t" + } + return strings.Repeat(ws, offset) +} diff --git a/formatter/chunk_buffer_test.go b/formatter/chunk_buffer_test.go new file mode 100644 index 00000000..f9c844c0 --- /dev/null +++ b/formatter/chunk_buffer_test.go @@ -0,0 +1,122 @@ +package formatter + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ysugimoto/falco/config" +) + +func TestChunkBuffer(t *testing.T) { + tests := []struct { + name string + maxLength int + input []string + expect string + }{ + { + name: "basic chunked string", + maxLength: 80, + input: []string{ + "lorem", + "ipsum", + "dolor", + "sit", + "amet,", + "consectetur", + "adipiscing", + "elit,", + "sed", + "do", + "eiusmod", + "tempor", + "incididunt", + "ut", + "labore", + "et", + "dolore", + "magna", + "aliqua", + }, + expect: `lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua`, + }, + { + name: "no linefeed", + maxLength: 80, + input: []string{ + "lorem", + "ipsum", + "dolor", + "sit", + "amet,", + "consectetur", + "adipiscing", + "elit,", + }, + expect: `lorem ipsum dolor sit amet, consectetur adipiscing elit,`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cb := newBuffer(&config.FormatConfig{ + LineWidth: tt.maxLength, + IndentWidth: 2, + }) + for _, c := range tt.input { + cb.WriteString(c) + } + chunk := cb.ChunkedString(0, 0) + if diff := cmp.Diff(tt.expect, chunk); diff != "" { + t.Errorf("Result mismatch, diff=%s", diff) + } + }) + } +} + +func TestChunkBufferInExpression(t *testing.T) { + tests := []struct { + name string + maxLength int + prefix string + input []string + expect string + }{ + { + name: "in set statement expression", + maxLength: 80, + prefix: "set req.http.Value = ", + input: []string{ + "req.http.Host", + "req.http.X-Forwarded-Host", + `if(req.http.Foo, "foo", "bar")`, + `{"lorem ipsum dolor sit amet"}`, + }, + expect: `set req.http.Value = req.http.Host req.http.X-Forwarded-Host + if(req.http.Foo, "foo", "bar") + {"lorem ipsum dolor sit amet"};`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + buf.WriteString(tt.prefix) + cb := newBuffer(&config.FormatConfig{ + LineWidth: tt.maxLength, + IndentWidth: 2, + }) + for _, c := range tt.input { + cb.WriteString(c) + } + chunk := cb.ChunkedString(0, buf.Len()) + buf.WriteString(chunk) + buf.WriteString(";") + if diff := cmp.Diff(tt.expect, buf.String()); diff != "" { + t.Errorf("Result mismatch, diff=%s", diff) + } + }) + } +} diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index e4ba09d1..ec628fb5 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -11,7 +11,6 @@ import ( func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("acl " + decl.Name.Value + " {\n") for _, cidr := range decl.CIDRs { buf.WriteString(f.formatComment(cidr.Leading, "\n", 1)) @@ -35,7 +34,6 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -43,7 +41,6 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("backend " + decl.Name.Value + " {\n") buf.WriteString(f.formatBackendProperties(decl.Properties, 1)) if len(decl.Infix) > 0 { @@ -51,7 +48,6 @@ func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) strin buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -78,18 +74,20 @@ func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLe for _, prop := range props { buf.WriteString(f.formatComment(prop.Leading, "\n", nestLevel)) buf.WriteString(f.indent(nestLevel)) + var left string if f.conf.AlignDeclarationProperty { format := fmt.Sprintf("%%-%ds", maxPropLength) - buf.WriteString(fmt.Sprintf("."+format+" = ", prop.Key.String())) + left = fmt.Sprintf("."+format+" = ", prop.Key.String()) } else { - buf.WriteString(fmt.Sprintf(".%s = ", prop.Key.String())) + left = fmt.Sprintf(".%s = ", prop.Key.String()) } + buf.WriteString(left) if po, ok := prop.Value.(*ast.BackendProbeObject); ok { buf.WriteString("{\n") buf.WriteString(f.formatBackendProperties(po.Values, nestLevel+1)) buf.WriteString(f.indent(nestLevel) + "}") } else { - buf.WriteString(f.formatExpression(prop.Value)) + buf.WriteString(f.formatExpression(prop.Value).ChunkedString(prop.Nest, len(left))) buf.WriteString(";") } buf.WriteString(f.trailing(prop.Trailing)) @@ -101,7 +99,6 @@ func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLe func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("director " + decl.Name.Value + " " + decl.DirectorType.Value + " {\n") for _, prop := range decl.Properties { buf.WriteString(f.indent(1)) @@ -113,7 +110,6 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) str buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -141,7 +137,6 @@ func (f *Formatter) formatDirectorProperty(prop *ast.DirectorBackendObject) stri func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("table " + decl.Name.Value) if decl.ValueType != nil { buf.WriteString(" " + decl.ValueType.Value) @@ -153,7 +148,6 @@ func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Trailing)) return buf.String() } @@ -183,7 +177,7 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { } else { buf.WriteString(fmt.Sprintf("%s: ", f.formatString(prop.Key))) } - buf.WriteString(f.formatExpression(prop.Value)) + buf.WriteString(f.formatExpression(prop.Value).String()) buf.WriteString(",") buf.WriteString(f.trailing(prop.Trailing)) buf.WriteString("\n") @@ -195,7 +189,6 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("penaltybox " + decl.Name.Value) buf.WriteString(" {\n") // penaltybox does not have properties @@ -204,7 +197,6 @@ func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Block.Trailing)) return buf.String() } @@ -212,7 +204,6 @@ func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("ratecounter " + decl.Name.Value) buf.WriteString(" {\n") // ratecounter does not have properties @@ -221,7 +212,6 @@ func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaratio buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(decl.Block.Trailing)) return buf.String() } @@ -229,9 +219,8 @@ func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaratio func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(decl.Leading, "\n", 0)) buf.WriteString("sub " + decl.Name.Value + " ") - buf.WriteString(f.formatBlockStatement(decl.Block, false)) + buf.WriteString(f.formatBlockStatement(decl.Block)) return buf.String() } diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 5bcdcd6e..94b19120 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -61,6 +61,7 @@ func TestAclDeclarationFormat(t *testing.T) { IndentStyle: "space", AclInverseWithSpace: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, } @@ -147,6 +148,7 @@ func TestBackendDeclarationFormat(t *testing.T) { IndentStyle: "space", AlignDeclarationProperty: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, { @@ -186,6 +188,7 @@ func TestBackendDeclarationFormat(t *testing.T) { AlignDeclarationProperty: true, SortDeclarationProperty: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, } @@ -236,6 +239,7 @@ func TestDirectorDeclarationFormat(t *testing.T) { IndentStyle: "space", SortDeclarationProperty: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, } @@ -300,6 +304,7 @@ func TestTableDeclarationFormat(t *testing.T) { IndentStyle: "space", SortDeclarationProperty: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, { @@ -320,6 +325,7 @@ func TestTableDeclarationFormat(t *testing.T) { IndentStyle: "space", AlignDeclarationProperty: true, TrailingCommentWidth: 2, + LineWidth: 80, }, }, } diff --git a/formatter/expression_format.go b/formatter/expression_format.go index b238d0b7..ed2571c8 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -1,242 +1,162 @@ package formatter import ( - "bytes" "fmt" "strings" "github.com/ysugimoto/falco/ast" ) -func (f *Formatter) formatExpression(expr ast.Expression) string { +func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { + buf := f.chunkBuffer() + + // leading comment + if v := f.formatComment(expr.GetMeta().Leading, "", 0); v != "" { + buf.WriteString(v + " ") + } + switch t := expr.(type) { + // Primitive types return string case *ast.Ident: - return f.formatIdent(t) + buf.WriteString(f.formatIdent(t)) case *ast.IP: - return f.formatIP(t) + buf.WriteString(f.formatIP(t)) case *ast.Boolean: - return f.formatBoolean(t) + buf.WriteString(f.formatBoolean(t)) case *ast.Integer: - return f.formatInteger(t) + buf.WriteString(f.formatInteger(t)) case *ast.String: - return f.formatString(t) + buf.WriteString(f.formatString(t)) case *ast.Float: - return f.formatFloat(t) + buf.WriteString(f.formatFloat(t)) case *ast.RTime: - return f.formatRTime(t) + buf.WriteString(f.formatRTime(t)) + + // Combinated expressions return *ChunkBuffer to merge case *ast.PrefixExpression: - return f.formatPrefixExpression(t) + buf.Merge(f.formatPrefixExpression(t)) case *ast.GroupedExpression: - return f.formatGroupedExpression(t) + buf.Merge(f.formatGroupedExpression(t)) case *ast.InfixExpression: - return f.formatInfixExpression(t) - case *ast.IfExpression: - return f.formatIfExpression(t) + buf.Merge(f.formatInfixExpression(t)) case *ast.FunctionCallExpression: - return f.formatFunctionCallExpression(t) - default: - return "" + buf.Merge(f.formatFunctionCallExpression(t)) + case *ast.IfExpression: + buf.Merge(f.formatIfExpression(t)) } -} - -func (f *Formatter) formatIdent(expr *ast.Ident) string { - var buf bytes.Buffer - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(expr.Value) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { + // trailing comment + if v := f.formatComment(expr.GetMeta().Trailing, "", 0); v != "" { buf.WriteString(" " + v) } - return buf.String() + + return buf } -func (f *Formatter) formatIP(expr *ast.IP) string { - var buf bytes.Buffer +// Primitive Expressions - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(expr.Value) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - return buf.String() +func (f *Formatter) formatIdent(expr *ast.Ident) string { + return expr.Value } -func (f *Formatter) formatBoolean(expr *ast.Boolean) string { - var buf bytes.Buffer +func (f *Formatter) formatIP(expr *ast.IP) string { + return expr.Value +} - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } +func (f *Formatter) formatBoolean(expr *ast.Boolean) string { str := fmt.Sprintf("%t", expr.Value) if f.conf.BoolUpperCase { - buf.WriteString(strings.ToUpper(str)) - } else { - buf.WriteString(strings.ToLower(str)) - } - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) + return strings.ToUpper(str) } - - return buf.String() + return strings.ToLower(str) } func (f *Formatter) formatInteger(expr *ast.Integer) string { - var buf bytes.Buffer - - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(fmt.Sprint(expr.Value)) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - - return buf.String() + return fmt.Sprint(expr.Value) } func (f *Formatter) formatFloat(expr *ast.Float) string { - var buf bytes.Buffer - - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(fmt.Sprint(expr.Value)) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - - return buf.String() + return fmt.Sprint(expr.Value) } func (f *Formatter) formatString(expr *ast.String) string { - var buf bytes.Buffer - - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } if expr.Token.Offset == 4 { // offset=4 means bracket string like {"..."} - buf.WriteString(fmt.Sprintf(`{"%s"}`, expr.Value)) - } else { - // Otherwise, double quoted string - buf.WriteString(fmt.Sprintf(`"%s"`, expr.Value)) + return fmt.Sprintf(`{"%s"}`, expr.Value) } - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - - return buf.String() + // Otherwise, double-quoted string + return fmt.Sprintf(`"%s"`, expr.Value) } func (f *Formatter) formatRTime(expr *ast.RTime) string { - var buf bytes.Buffer - - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(expr.Value) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - - return buf.String() + return expr.Value } -func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) string { - var buf bytes.Buffer +// Combinated expressions + +func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuffer { + buf := f.chunkBuffer() - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } buf.WriteString(expr.Operator) - buf.WriteString(f.formatExpression(expr.Right)) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } + buf.Merge(f.formatExpression(expr.Right)) - return buf.String() + return buf } -func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) string { - var buf bytes.Buffer - - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString(f.formatExpression(expr.Left)) +func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffer { + buf := f.chunkBuffer() + operator := expr.Operator if expr.Operator == "+" { // concatenation - if f.conf.ExplicitStringConat { - buf.WriteString(" + ") - } else { - buf.WriteString(" ") + if !f.conf.ExplicitStringConat { + operator = "" } - } else { - buf.WriteString(" " + expr.Operator + " ") } - buf.WriteString(f.formatExpression(expr.Right)) - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) + + buf.Merge(f.formatExpression(expr.Left)) + if operator != "" { + buf.WriteString(operator) } + buf.Merge(f.formatExpression(expr.Right)) - return buf.String() + return buf } -func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { - var buf bytes.Buffer +func (f *Formatter) formatIfExpression(expr *ast.IfExpression) *ChunkBuffer { + buf := f.chunkBuffer() - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } buf.WriteString("if(") - buf.WriteString(f.formatExpression(expr.Condition)) + buf.Merge(f.formatExpression(expr.Condition)) buf.WriteString(", ") - buf.WriteString(f.formatExpression(expr.Consequence)) + buf.Merge(f.formatExpression(expr.Consequence)) buf.WriteString(", ") - buf.WriteString(f.formatExpression(expr.Alternative)) + buf.Merge(f.formatExpression(expr.Alternative)) buf.WriteString(")") - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } - return buf.String() + return buf } -func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) string { - var buf bytes.Buffer +func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkBuffer { + buf := f.chunkBuffer() - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } - buf.WriteString("(" + f.formatExpression(expr.Right) + ")") - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(" " + v) - } + buf.WriteString("(") + buf.Merge(f.formatExpression(expr.Right)) + buf.WriteString(")") - return buf.String() + return buf } -func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) string { - var buf bytes.Buffer +func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) *ChunkBuffer { + buf := f.chunkBuffer() - if v := f.formatComment(expr.Leading, "", 0); v != "" { - buf.WriteString(v + " ") - } buf.WriteString(expr.Function.Value + "(") for i, arg := range expr.Arguments { - buf.WriteString(f.formatExpression(arg)) + buf.Merge(f.formatExpression(arg)) if i != len(expr.Arguments)-1 { buf.WriteString(", ") } } buf.WriteString(")") - if v := f.formatComment(expr.Trailing, "", 0); v != "" { - buf.WriteString(v + " ") - } - return buf.String() + return buf } diff --git a/formatter/formatter.go b/formatter/formatter.go index 801f4df1..a0811bbe 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -19,12 +19,19 @@ func New(conf *config.FormatConfig) *Formatter { } } +func (f *Formatter) chunkBuffer() *ChunkBuffer { + return newBuffer(f.conf) +} + func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { buf := new(bytes.Buffer) - var formatted string - for i := range vcl.Statements { - switch t := vcl.Statements[i].(type) { + for i, stmt := range vcl.Statements { + buf.WriteString(f.formatComment(stmt.GetMeta().Leading, "\n", 0)) + var formatted string + trailingNode := stmt + + switch t := stmt.(type) { case *ast.ImportStatement: formatted = f.formatImportStatement(t) case *ast.IncludeStatement: @@ -37,24 +44,32 @@ func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { formatted = f.formatDirectorDeclaration(t) case *ast.TableDeclaration: formatted = f.formatTableDeclaration(t) + + // penaltybox, ratecounter, and subroutine have trailing comment on the block statement case *ast.PenaltyboxDeclaration: formatted = f.formatPenaltyboxDeclaration(t) + trailingNode = t.Block case *ast.RatecounterDeclaration: formatted = f.formatRatecounterDeclaration(t) + trailingNode = t.Block case *ast.SubroutineDeclaration: formatted = f.formatSubroutineDeclaration(t) + trailingNode = t.Block } - buf.WriteString(formatted + "\n") + buf.WriteString(formatted) + buf.WriteString(f.trailing(trailingNode.GetMeta().Trailing)) + buf.WriteString("\n") if i != len(vcl.Statements)-1 { buf.WriteString("\n") } } + return bytes.NewReader(buf.Bytes()), nil } func (f *Formatter) indent(level int) string { c := " " // default as whitespace - if f.conf.IndentStyle == "tab" { + if f.conf.IndentStyle == config.IndentStyleTab { c = "\t" } return strings.Repeat(c, level*f.conf.IndentWidth) diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go index 5c8e808f..57c1a1ad 100644 --- a/formatter/formatter_test.go +++ b/formatter/formatter_test.go @@ -15,6 +15,7 @@ func assert(t *testing.T, input, expect string, conf *config.FormatConfig) strin IndentWidth: 2, IndentStyle: "space", TrailingCommentWidth: 2, + LineWidth: 80, } if conf != nil { c = conf diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 5046bbec..a939055d 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -2,67 +2,95 @@ package formatter import ( "bytes" + "strings" "github.com/ysugimoto/falco/ast" ) func (f *Formatter) formatStatement(stmt ast.Statement) string { + var buf bytes.Buffer + + if block, ok := stmt.(*ast.BlockStatement); ok { + // need subtract 1 because LEFT_BRACE is unnested + buf.WriteString(f.formatComment(block.Leading, "\n", block.Nest-1)) + buf.WriteString(f.indent(block.Nest - 1)) + buf.WriteString(f.formatBlockStatement(block)) + buf.WriteString(f.trailing(block.Trailing)) + return buf.String() + } + + buf.WriteString(f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest)) + buf.WriteString(f.indent(stmt.GetMeta().Nest)) + + var formatted string + trailingNode := stmt switch t := stmt.(type) { - case *ast.BlockStatement: - return f.formatBlockStatement(t, true) case *ast.ImportStatement: - return f.formatImportStatement(t) + formatted = f.formatImportStatement(t) case *ast.IncludeStatement: - return f.formatIncludeStatement(t) + formatted = f.formatIncludeStatement(t) case *ast.DeclareStatement: - return f.formatDeclareStatement(t) + formatted = f.formatDeclareStatement(t) case *ast.SetStatement: - return f.formatSetStatement(t) + formatted = f.formatSetStatement(t) case *ast.UnsetStatement: - return f.formatUnsetStatement(t) + formatted = f.formatUnsetStatement(t) case *ast.RemoveStatement: - return f.formatRemoveStatement(t) - case *ast.IfStatement: - return f.formatIfStatement(t) + formatted = f.formatRemoveStatement(t) case *ast.SwitchStatement: - return f.formatSwitchStatement(t) + formatted = f.formatSwitchStatement(t) case *ast.RestartStatement: - return f.formatRestartStatement(t) + formatted = f.formatRestartStatement() case *ast.EsiStatement: - return f.formatEsiStatement(t) + formatted = f.formatEsiStatement() case *ast.AddStatement: - return f.formatAddStatement(t) + formatted = f.formatAddStatement(t) case *ast.CallStatement: - return f.formatCallStatement(t) + formatted = f.formatCallStatement(t) case *ast.ErrorStatement: - return f.formatErrorStatement(t) + formatted = f.formatErrorStatement(t) case *ast.LogStatement: - return f.formatLogStatement(t) + formatted = f.formatLogStatement(t) case *ast.ReturnStatement: - return f.formatReturnStatement(t) + formatted = f.formatReturnStatement(t) case *ast.SyntheticStatement: - return f.formatSyntheticStatement(t) + formatted = f.formatSyntheticStatement(t) case *ast.SyntheticBase64Statement: - return f.formatSyntheticBase64Statement(t) + formatted = f.formatSyntheticBase64Statement(t) case *ast.GotoStatement: - return f.formatGotoStatement(t) + formatted = f.formatGotoStatement(t) case *ast.GotoDestinationStatement: - return f.formatGotoDestinationStatement(t) + formatted = f.formatGotoDestinationStatement(t) case *ast.FunctionCallStatement: - return f.formatFunctionCallStatement(t) + formatted = f.formatFunctionCallStatement(t) + + // On if statement, trailing comment node depends on its declarations + case *ast.IfStatement: + formatted = f.formatIfStatement(t) + switch { + case t.Alternative != nil: + // When "else" statament exists, trailing comment will be on it + trailingNode = t.Alternative + case len(t.Another) > 0: + // When one of "else if" statament exists, trailing comment will be on it + trailingNode = t.Another[len(t.Another)-1] + default: + // Otherwise, trailing comment will be on consequence + trailingNode = t.Consequence + } } - return "" + buf.WriteString(formatted) + buf.WriteString(f.trailing(trailingNode.GetMeta().Trailing)) + + return buf.String() } func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("import ") buf.WriteString(stmt.Name.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -70,26 +98,17 @@ func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("include ") buf.WriteString(f.formatString(stmt.Module)) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } -func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement, isIndependent bool) string { +func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { var buf bytes.Buffer - if isIndependent { - // need subtract 1 because LEFT_BRACE is unnested - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest-1)) - buf.WriteString(f.indent(stmt.Meta.Nest - 1)) - } buf.WriteString("{\n") - for i := range stmt.Statements { buf.WriteString(f.formatStatement(stmt.Statements[i])) buf.WriteString("\n") @@ -100,7 +119,6 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement, isIndependent // need subtract 1 because RIGHT_BRACE is unnested buf.WriteString(f.indent(stmt.Meta.Nest - 1)) buf.WriteString("}") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -108,12 +126,9 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement, isIndependent func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("declare local " + stmt.Name.Value) buf.WriteString(" " + stmt.ValueType.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -121,13 +136,10 @@ func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("set " + stmt.Ident.Value) buf.WriteString(" " + stmt.Operator.Operator + " ") - buf.WriteString(f.formatExpression(stmt.Value)) + buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -135,11 +147,8 @@ func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("unset " + stmt.Ident.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -147,11 +156,8 @@ func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("remove " + stmt.Ident.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -159,10 +165,15 @@ func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString(stmt.Keyword + " (" + f.formatExpression(stmt.Condition) + ") ") - buf.WriteString(f.formatBlockStatement(stmt.Consequence, false)) + buf.WriteString(stmt.Keyword + " (") + chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, buf.Len()) + if strings.Contains(chunk, "\n") { + buf.WriteString("\n" + chunk + "\n") + } else { + buf.WriteString(chunk) + } + buf.WriteString(") ") + buf.WriteString(f.formatBlockStatement(stmt.Consequence)) for _, a := range stmt.Another { // If leading comments exists, keyword should be placed with line-feed if len(a.Leading) > 0 { @@ -178,8 +189,15 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { if f.conf.ElseIf { keyword = "else if" } - buf.WriteString(keyword + " (" + f.formatExpression(a.Condition) + ") ") - buf.WriteString(f.formatBlockStatement(a.Consequence, false)) + chunk := f.formatExpression(a.Condition).ChunkedString(a.Nest, len(keyword)+2) + buf.WriteString(keyword + " (") + if strings.Contains(chunk, "\n") { + buf.WriteString("\n" + chunk + "\n") + } else { + buf.WriteString(chunk) + } + buf.WriteString(") ") + buf.WriteString(f.formatBlockStatement(a.Consequence)) } if stmt.Alternative != nil { if len(stmt.Alternative.Leading) > 0 { @@ -190,9 +208,8 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { buf.WriteString(" ") } buf.WriteString("else ") - buf.WriteString(f.formatBlockStatement(stmt.Alternative, false)) + buf.WriteString(f.formatBlockStatement(stmt.Alternative)) } - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -200,9 +217,7 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString("switch (" + f.formatExpression(stmt.Control) + ") {\n") + buf.WriteString("switch (" + f.formatExpression(stmt.Control).String() + ") {\n") for _, c := range stmt.Cases { buf.WriteString(f.formatComment(c.Leading, "\n", c.Meta.Nest)) buf.WriteString(f.indent(c.Meta.Nest)) @@ -211,7 +226,7 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { if c.Test.Operator == "~" { buf.WriteString("~ ") } - buf.WriteString(f.formatExpression(c.Test.Right)) + buf.WriteString(f.formatExpression(c.Test.Right).String()) buf.WriteString(":\n") } else { buf.WriteString("default:\n") @@ -235,29 +250,22 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { } buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("}") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } -func (f *Formatter) formatRestartStatement(stmt *ast.RestartStatement) string { +func (f *Formatter) formatRestartStatement() string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("restart;") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } -func (f *Formatter) formatEsiStatement(stmt *ast.EsiStatement) string { +func (f *Formatter) formatEsiStatement() string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("esi;") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -265,13 +273,10 @@ func (f *Formatter) formatEsiStatement(stmt *ast.EsiStatement) string { func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("add " + stmt.Ident.Value) buf.WriteString(" " + stmt.Operator.Operator + " ") - buf.WriteString(f.formatExpression(stmt.Value)) + buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -279,11 +284,8 @@ func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("call " + stmt.Subroutine.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -291,14 +293,11 @@ func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString("error " + f.formatExpression(stmt.Code)) + buf.WriteString("error " + f.formatExpression(stmt.Code).String()) if stmt.Argument != nil { - buf.WriteString(" " + f.formatExpression(stmt.Argument)) + buf.WriteString(" " + f.formatExpression(stmt.Argument).String()) } buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -306,11 +305,9 @@ func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString("log " + f.formatExpression(stmt.Value)) + buf.WriteString("log ") + buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -318,8 +315,6 @@ func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("return") if stmt.ReturnExpression != nil { prefix := " " @@ -329,11 +324,10 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { suffix = ")" } buf.WriteString(prefix) - buf.WriteString(f.formatExpression(*stmt.ReturnExpression)) + buf.WriteString(f.formatExpression(*stmt.ReturnExpression).String()) buf.WriteString(suffix) } buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -341,11 +335,9 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString("synthetic " + f.formatExpression(stmt.Value)) + buf.WriteString("synthetic ") + buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -353,11 +345,9 @@ func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) strin func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Statement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) - buf.WriteString("synthetic.base64 " + f.formatExpression(stmt.Value)) + buf.WriteString("synthetic.base64 ") + buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -365,11 +355,8 @@ func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Stat func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("goto " + stmt.Destination.Value) buf.WriteString(";") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } @@ -377,27 +364,23 @@ func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { func (f *Formatter) formatGotoDestinationStatement(stmt *ast.GotoDestinationStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString(stmt.Name.Value) - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } + func (f *Formatter) formatFunctionCallStatement(stmt *ast.FunctionCallStatement) string { var buf bytes.Buffer - buf.WriteString(f.formatComment(stmt.Leading, "\n", stmt.Meta.Nest)) - buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString(stmt.Function.Value + "(") + length := buf.Len() for i, a := range stmt.Arguments { - buf.WriteString(f.formatExpression(a)) + buf.WriteString(f.formatExpression(a).ChunkedString(stmt.Nest, length)) if i != len(stmt.Arguments)-1 { buf.WriteString(", ") } } buf.WriteString(");") - buf.WriteString(f.trailing(stmt.Trailing)) return buf.String() } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 07470c9f..87502c5e 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -591,6 +591,13 @@ func TestFormatSynthticBase64Statement(t *testing.T) { synthetic.base64 "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo="; // synthetic.base64 trailing comment } `, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + SortDeclarationProperty: true, + TrailingCommentWidth: 2, + LineWidth: 120, + }, }, } From f6f548deb1ef9349cedc95c6bcb313736313cb3e Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 27 Mar 2024 23:30:47 +0900 Subject: [PATCH 07/22] modify chunking, documentation --- .gitignore | 3 + README.md | 10 + cmd/falco/help.go | 26 +- cmd/falco/main.go | 25 +- cmd/falco/runner.go | 32 +++ config/config.go | 32 ++- config/config_test.go | 21 +- docs/formatter.md | 345 +++++++++++++++++++++++++++ formatter/declaration_format.go | 3 - formatter/declaration_format_test.go | 21 -- formatter/expression_format.go | 68 ++++-- formatter/formatter.go | 4 +- formatter/formatter_test.go | 8 +- formatter/statement_format.go | 22 +- formatter/statement_format_test.go | 40 +++- resolver/file.go | 6 +- resolver/glob.go | 50 ++++ 17 files changed, 608 insertions(+), 108 deletions(-) create mode 100644 docs/formatter.md create mode 100644 resolver/glob.go diff --git a/.gitignore b/.gitignore index ef78edb3..ffd0202c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ __generator__/*.csv debugger.log debug.log request.json + +.falco.yml +.falco.yaml diff --git a/README.md b/README.md index 219ef29e..260ed263 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ Subcommands: stats : Analyze VCL statistics simulate : Run simulator server with provided VCLs test : Run local testing for provided VCLs + fmt : Run formatter for provided VCLs + See subcommands help with: falco [subcommand] -h @@ -93,6 +95,14 @@ that you improve your VCL more robustly by passing the linter. See [linter documentation](https://github.com/ysugimoto/falco/blob/main/docs/linter.md) in detail. +## Formatter + +Format provided VCL by our recommended styles. +Currently we have a few options to control formatting style like [biomejs](https://github.com/biomejs/biome). +Through the formatter, your VCL codes have unified format even multiple people are maintaining VCL. + +See [formatter documentation](./docs/formatter.md) in detail. + ## Local Simulator / VCL Debugger `falco` has self-implemented interpreter for running VCL program locally. diff --git a/cmd/falco/help.go b/cmd/falco/help.go index c0b2e013..0d8ff3a6 100644 --- a/cmd/falco/help.go +++ b/cmd/falco/help.go @@ -17,6 +17,8 @@ func printHelp(cmd string) { printTestHelp() case subcommandLint: printLintHelp() + case subcommandFormat: + printFormatHelp() default: printGlobalHelp() } @@ -46,6 +48,7 @@ Subcommands: stats : Analyze VCL statistics simulate : Run simulator server with provided VCLs test : Run local testing for provided VCLs + fmt : Run formatter for provided VCLs See subcommands help with: falco [subcommand] -h @@ -91,7 +94,7 @@ Linting with terraform: func printSimulateHelp() { writeln(white, strings.TrimSpace(` Usage: - falco simulate [flags] + falco simulate [flags] file Flags: -I, --include_path : Add include path @@ -113,7 +116,7 @@ Local debugger example: func printStatsHelp() { writeln(white, strings.TrimSpace(` Usage: - falco stats [flags] + falco stats [flags] file Flags: -I, --include_path : Add include path @@ -150,13 +153,12 @@ Local testing example: func printLintHelp() { writeln(white, strings.TrimSpace(` Usage: - falco lint [flags] + falco lint [flags] file Flags: -I, --include_path : Add include path -h, --help : Show this help -r, --remote : Connect with Fastly API - -V, --version : Display build version -v : Output lint warnings (verbose) -vv : Output all lint results (very verbose) -json : Output results as JSON (very verbose) @@ -165,3 +167,19 @@ Simple linting with very verbose example: falco lint -I . -vv /path/to/vcl/main.vcl `)) } + +func printFormatHelp() { + writeln(white, strings.TrimSpace(` +Usage: + falco fmt [flags] ...files + +Flags: + -h, --help : Show this help + -w, --write : Overwrite format result + +files argument accepts glob file patterns + +Simple format example: + falco fmt /path/to/vcl/main.vcl + `)) +} diff --git a/cmd/falco/main.go b/cmd/falco/main.go index 4eb0ce8b..06b5e07f 100644 --- a/cmd/falco/main.go +++ b/cmd/falco/main.go @@ -51,6 +51,7 @@ const ( subcommandSimulate = "simulate" subcommandStats = "stats" subcommandTest = "test" + subcommandFormat = "fmt" ) func write(c *color.Color, format string, args ...interface{}) { @@ -88,10 +89,17 @@ func main() { } action = c.Commands.At(1) case subcommandSimulate, subcommandLint, subcommandStats, subcommandTest: - // "lint", "simulate", "stats" and "test" command provides single file of service, + // "lint", "simulate", "stats", and "test" command provides single file of service, // then resolvers size is always 1 resolvers, err = resolver.NewFileResolvers(c.Commands.At(1), c.IncludePaths) action = c.Commands.At(0) + case subcommandFormat: + // "fmt" command accepts multiple target files + resolvers, err = resolver.NewGlobResolver(c.Commands[1:]...) + action = c.Commands.At(0) + if len(resolvers) == 0 { + err = fmt.Errorf("No input files speficied") + } case "": printHelp("") os.Exit(1) @@ -105,7 +113,8 @@ func main() { } } - if c.Remote { + // No need to use remove object on fmt command + if action != subcommandFormat && c.Remote { if !c.Json { writeln(cyan, "Remote option supplied. Fetching snippets from Fastly.") } @@ -154,6 +163,8 @@ func main() { exitErr = runSimulate(runner, v) case subcommandStats: exitErr = runStats(runner, v) + case subcommandFormat: + exitErr = runFormat(runner, v) default: exitErr = runLint(runner, v) } @@ -377,3 +388,13 @@ func runTest(runner *Runner, rslv resolver.Resolver) error { return nil } } + +func runFormat(runner *Runner, rslv resolver.Resolver) error { + if err := runner.Format(rslv); err != nil { + if err != ErrParser { + writeln(red, err.Error()) + } + return ErrExit + } + return nil +} diff --git a/cmd/falco/runner.go b/cmd/falco/runner.go index 99715e15..ee3ce7fd 100644 --- a/cmd/falco/runner.go +++ b/cmd/falco/runner.go @@ -3,7 +3,9 @@ package main import ( "bytes" "fmt" + "io" "net/http" + "os" "strings" "github.com/fatih/color" @@ -12,6 +14,7 @@ import ( "github.com/ysugimoto/falco/config" "github.com/ysugimoto/falco/context" "github.com/ysugimoto/falco/debugger" + "github.com/ysugimoto/falco/formatter" "github.com/ysugimoto/falco/interpreter" icontext "github.com/ysugimoto/falco/interpreter/context" "github.com/ysugimoto/falco/lexer" @@ -485,3 +488,32 @@ func (r *Runner) Test(rslv resolver.Resolver) (*tester.TestFactory, error) { r.message(white, " Done.\n") return factory, nil } + +func (r *Runner) Format(rslv resolver.Resolver) error { + main, err := rslv.MainVCL() + if err != nil { + return err + } + vcl, err := r.parseVCL(main.Name, main.Data) + if err != nil { + return err + } + + formatted := formatter.New(r.config.Format).Format(vcl) + var w io.Writer + if r.config.Format.Overwrite { + writeln(cyan, "Formatted %s.", main.Name) + fp, err := os.OpenFile(main.Name, os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return errors.WithStack(err) + } + defer fp.Close() + w = fp + } else { + w = os.Stdout + } + if _, err := io.Copy(w, formatted); err != nil { + return err + } + return nil +} diff --git a/config/config.go b/config/config.go index 3fe439ec..b89f6d90 100644 --- a/config/config.go +++ b/config/config.go @@ -54,18 +54,20 @@ type TestConfig struct { // Format configuration type FormatConfig struct { - IndentWidth int `yaml:"indent_Width" default:"2"` - TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` - IndentStyle string `yaml:"indent_style" default:"space"` - LineWidth int `yaml:"line_width" default:"80"` - BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` - ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` - // FormatComment bool `yaml:"format_comment" default:"true"` - SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` - AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` - AclInverseWithSpace bool `yaml:"acl_inverse_with_space" default:"false"` - ElseIf bool `yaml:"else_if" default:"false"` - ReturnArgumentParenthesis bool `yaml:"return_argument_parenthesis" default:"true"` + // CLI options + Overwrite bool `cli:"w,write" default:"false"` + + // Formatter options + IndentWidth int `yaml:"indent_width" default:"2"` + TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` + IndentStyle string `yaml:"indent_style" default:"space"` + LineWidth int `yaml:"line_width" default:"120"` + BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` + ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` + SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` + AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` + ElseIf bool `yaml:"else_if" default:"false"` + ReturnStatementParenthesis bool `yaml:"return_statement_parenthesis" default:"true"` } type Config struct { @@ -115,12 +117,6 @@ func New(args []string) (*Config, error) { c := &Config{ OverrideBackends: make(map[string]*OverrideBackend), - // Simulator: &SimulatorConfig{ - // OverrideRequest: &RequestConfig{}, - // }, - // Testing: &TestConfig{ - // OverrideRequest: &RequestConfig{}, - // }, } if err := twist.Mix(c, options...); err != nil { return nil, errors.WithStack(err) diff --git a/config/config_test.go b/config/config_test.go index 80e89531..b11bec53 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -64,17 +64,16 @@ func TestConfigFromCLI(t *testing.T) { OverrideRequest: &RequestConfig{}, }, Format: &FormatConfig{ - IndentWidth: 2, - TrailingCommentWidth: 2, - LineWidth: 80, - IndentStyle: "space", - BoolUpperCase: false, - ExplicitStringConat: false, - SortDeclarationProperty: false, - AlignDeclarationProperty: false, - AclInverseWithSpace: false, - ElseIf: false, - ReturnArgumentParenthesis: true, + IndentWidth: 2, + TrailingCommentWidth: 2, + LineWidth: 120, + IndentStyle: "space", + BoolUpperCase: false, + ExplicitStringConat: false, + SortDeclarationProperty: false, + AlignDeclarationProperty: false, + ElseIf: false, + ReturnStatementParenthesis: true, }, OverrideBackends: make(map[string]*OverrideBackend), } diff --git a/docs/formatter.md b/docs/formatter.md new file mode 100644 index 00000000..87b7191a --- /dev/null +++ b/docs/formatter.md @@ -0,0 +1,345 @@ +# Formatting VCL + +You can format VCL to get unified styled VCL. + +## Usage + +``` +falco fmt -h +========================================================= + ____ __ + / __/______ / /_____ ____ + / /_ / __ // // __// __ \ + / __// /_/ // // /__ / /_/ / +/_/ \____//_/ \___/ \____/ Fastly VCL developer tool + +========================================================= +Usage: + falco fmt [flags] ...files + +Flags: + -h, --help : Show this help + -w, --write : Overwrite format result + +files argument accepts glob file patterns + +Simple format example: + falco fmt /path/to/vcl/main.vcl +``` + +Simply you can run formatter as following: + +```shell +falco fmt /path/to/your/default.vcl +``` + +And `fmt` command accepts multiple source files including glob patterns like: + +```shell +falco fmt /path/to/your/*.vcl /path/to/another/**/*.vcl +``` + +## Format rules + +Formatting rules have default parameter which we recommend but you can override them with `format` section in configuration file. +See [configuration documentation](https://github.com/ysugimoto/falco/blob/develop/docs/configuration.md) in detail. + +Supporting rules are described the following table and sections. + +| Name (configuration field) | Type | Default | Description | +|:----------------------------|:------:|:-------:|------------------------------------------------------------------------------------------------| +| indent_width | INT | 2 | Specify indent width | +| indent_style | STRING | space | Specify indent style character. `space(whitespace)` or `tab(\t)` is accepted | +| trailing_comment_width | INT | 2 | Specify space size for trailing comment | +| line_width | INT | 120 | Specify max characters for each line. The overflowed characters are displayed at the next line | +| explicit_string_concat | BOOL | false | Explicitly write string concatenation operator `+` between expressions | +| sort_declaration_property | BOOL | false | If true, sort declaration properties like table, backend and director alphabetecally | +| align_declaration_property | BOOL | false | If true, align declaration properties like table, backend and director | +| else_if | BOOL | false | Coerce use `else if` keyword for another if statement | +| return_statement_parentheis | BOOL | true | Coerce surrounded return statement ident by parenthesis | + + +### Indent Width + +**default: 2** + +Define the indent width by unit. the formatted statements/propertied will have indent by provided width with `Indent Style` character. + +Before: + +```vcl +sub vcl_recv { +set req.http.Foo = "bar"; +} +``` + +Formatted (indent_width: 2): + +```vcl +sub vcl_recv { + set req.http.Foo = "bar"; +} +``` + +--- + +### Indent Style + +**default: space** + +Define the indent style. `space` value will use whitespace `" "` character, or `tab` value will use tab `\t` character. +And this character is repeated by `Indent Width` times. + +```vcl +sub vcl_recv { +set req.http.Foo = "bar"; +} +``` + +Formatted (indent_style: space): + +```vcl +sub vcl_recv { + set req.http.Foo = "bar"; +} +``` + +--- + +### Trailing Comment Width + +**default: 2** + +Define trailing comment space size. + +```vcl +sub vcl_recv { + set req.http.Foo = "bar"; +}// trailing comment +``` + +Formatted (trailing_comment_width: 2): + +```vcl +sub vcl_recv { + set req.http.Foo = "bar"; +} // trailing comment +``` + +--- + +### Line Width + +**default: 120** + +Specify max characters for each line. The overflowed characters are printed at the next line with the same ident. + +[!IMPORTANT] +Inserting line-feed is judged for each expression. It means formatter does not split in the middle of a sentence. + +```vcl +sub vcl_recv { + set req.http.Foo = "lorem" req.http.Sep "ipsum" req.http.Sep "dolor" req.http.Sep "sit" req.http.Sep "amet,"; +} +``` + +Formatted (line_width: 80): + +```vcl +sub vcl_recv { + set req.http.Foo = "lorem" req.http.Sep "ipsum" req.http.Sep "dolor" + req.http.Sep "sit" req.http.Sep "amet,"; +} +``` + +If condition example: + +```vcl +sub vcl_recv { + if (req.http.Foo == "bar" && req.http.Bar = "baz" && req.http.Baz == "foo") { + req.http.OK = "1"; + } +} +``` + +Formatted (line_width: 80): + +```vcl +sub vcl_recv { + if ( + req.http.Header1 == "1" && req.http.Header2 == "2" && + req.http.Header3 == "3" && req.http.Header4 == "4" + ) { + set req.http.OK = "1"; + } +} +``` + +--- + +### Explicit Declaration Contact + +**default: false** + +Print string concatenation character of `+` explicitly if true. + +```vcl +sub vcl_recv { + set req.http.Foo = "lorem" "ipsum" "dolor" "sit" "amet,"; +} +``` + +Formatted (explicit_string_concat: true): + +```vcl +sub vcl_recv { + set req.http.Foo = "lorem" + "ipsum" + "dolor" + "sit" + "amet,"; +} +``` + +--- + +### Sort Declaration Property + +**default: false** + +If true, declaration properties will be printed with alphabetecally sorted. +This rule affects to `backend`, `director`, and `table` declarations. + +```vcl +backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } +} +``` + +Formatted (sort_declaration_property: true): + +```vcl +backend example { + .between_bytes_timeout = 30s; + .connect_timeout = 1s; + .dynamic = true; + .first_byte_timeout = 30s; + .host = "example.com"; + .max_connections = 500; + .port = "443"; + .ssl = true; + .probe = { + .dummy = true; + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + } +} +``` + +--- + +### Align Declaration Property + +**default: false** + +If true, declaration properties will be aligned for max field character length. +This rule affects to `backend`, `director`, and `table` declarations. + +```vcl +backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } +}`, +``` + +Formatted (align_declaration_property: true): + +```vcl +backend example { + .connect_timeout = 1s; + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } +} +``` + +--- + +### Else If + +**default: false** + +VCL accepts a little kind of another if statement keyword like `else if`, `elseif`, and `elsif`, sometimes it's annoying to recognize. +If this rule set `true`, print these keyword as `else if`. + +```vcl +sub vcl_recv { + if (req.http.Foo) { + req.http.Status = "1"; + } elseif (req.http.Bar) { + req.http.Status = "2"; + } elsif (req.http.Baz) { + req.http.Status = "3"; + } +} +``` + +Formatted (else_if: true): + +```vcl +sub vcl_recv { + if (req.http.Foo) { + req.http.Status = "1"; + } else if (req.http.Bar) { + req.http.Status = "2"; + } else if (req.http.Baz) { + req.http.Status = "3"; + } +} +``` + +--- + +### Return Statement Parenthesis + +**default: true** + +If true, coerce surrounding parenthesis state ident of `return` statement. + +```vcl +sub vcl_recv { + return lookup; +} +``` + +Formatted (recommend: true): + +```vcl +sub vcl_recv { + return (lookup); +} +``` diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index ec628fb5..68f6069c 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -17,9 +17,6 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { buf.WriteString(f.indent(1)) if cidr.Inverse != nil && cidr.Inverse.Value { buf.WriteString("!") - if f.conf.AclInverseWithSpace { - buf.WriteString(" ") - } } buf.WriteString(`"` + cidr.IP.String() + `"`) if cidr.Mask != nil { diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 94b19120..22afcf79 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -43,27 +43,6 @@ func TestAclDeclarationFormat(t *testing.T) { } // trailing `, }, - { - name: "with inverse spacing", - input: `acl name { - "192.0.2.0"/24; // some comment - !"192.0.2.12"; - "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; - }`, - expect: `acl name { - "192.0.2.0"/24; // some comment - ! "192.0.2.12"; - "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; -} -`, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - AclInverseWithSpace: true, - TrailingCommentWidth: 2, - LineWidth: 80, - }, - }, } for _, tt := range tests { diff --git a/formatter/expression_format.go b/formatter/expression_format.go index ed2571c8..9f38cd4d 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -1,8 +1,8 @@ package formatter import ( + "bytes" "fmt" - "strings" "github.com/ysugimoto/falco/ast" ) @@ -31,6 +31,10 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { buf.WriteString(f.formatFloat(t)) case *ast.RTime: buf.WriteString(f.formatRTime(t)) + case *ast.FunctionCallExpression: + buf.WriteString(f.formatFunctionCallExpression(t)) + case *ast.IfExpression: + buf.WriteString(f.formatIfExpression(t)) // Combinated expressions return *ChunkBuffer to merge case *ast.PrefixExpression: @@ -39,10 +43,6 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { buf.Merge(f.formatGroupedExpression(t)) case *ast.InfixExpression: buf.Merge(f.formatInfixExpression(t)) - case *ast.FunctionCallExpression: - buf.Merge(f.formatFunctionCallExpression(t)) - case *ast.IfExpression: - buf.Merge(f.formatIfExpression(t)) } // trailing comment @@ -64,11 +64,7 @@ func (f *Formatter) formatIP(expr *ast.IP) string { } func (f *Formatter) formatBoolean(expr *ast.Boolean) string { - str := fmt.Sprintf("%t", expr.Value) - if f.conf.BoolUpperCase { - return strings.ToUpper(str) - } - return strings.ToLower(str) + return fmt.Sprintf("%t", expr.Value) } func (f *Formatter) formatInteger(expr *ast.Integer) string { @@ -103,6 +99,17 @@ func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuf return buf } +var mustSingleOperators = map[string]struct{}{ + "==": {}, + "!=": {}, + "~": {}, + "!~": {}, + ">": {}, + "<": {}, + ">=": {}, + "<=": {}, +} + func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffer { buf := f.chunkBuffer() @@ -113,27 +120,38 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe } } - buf.Merge(f.formatExpression(expr.Left)) - if operator != "" { - buf.WriteString(operator) + if _, ok := mustSingleOperators[operator]; ok { + buf.WriteString( + fmt.Sprintf( + "%s %s %s", + f.formatExpression(expr.Left).String(), + operator, + f.formatExpression(expr.Right).String(), + ), + ) + } else { + // Can split to newline + buf.Merge(f.formatExpression(expr.Left)) + if operator != "" { + buf.WriteString(operator) + } + buf.Merge(f.formatExpression(expr.Right)) } - buf.Merge(f.formatExpression(expr.Right)) - return buf } -func (f *Formatter) formatIfExpression(expr *ast.IfExpression) *ChunkBuffer { - buf := f.chunkBuffer() +func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { + var buf bytes.Buffer buf.WriteString("if(") - buf.Merge(f.formatExpression(expr.Condition)) + buf.WriteString(f.formatExpression(expr.Condition).String()) buf.WriteString(", ") - buf.Merge(f.formatExpression(expr.Consequence)) + buf.WriteString(f.formatExpression(expr.Consequence).String()) buf.WriteString(", ") - buf.Merge(f.formatExpression(expr.Alternative)) + buf.WriteString(f.formatExpression(expr.Alternative).String()) buf.WriteString(")") - return buf + return buf.String() } func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkBuffer { @@ -146,17 +164,17 @@ func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkB return buf } -func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) *ChunkBuffer { - buf := f.chunkBuffer() +func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) string { + var buf bytes.Buffer buf.WriteString(expr.Function.Value + "(") for i, arg := range expr.Arguments { - buf.Merge(f.formatExpression(arg)) + buf.WriteString(f.formatExpression(arg).String()) if i != len(expr.Arguments)-1 { buf.WriteString(", ") } } buf.WriteString(")") - return buf + return buf.String() } diff --git a/formatter/formatter.go b/formatter/formatter.go index a0811bbe..6abb1c43 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -23,7 +23,7 @@ func (f *Formatter) chunkBuffer() *ChunkBuffer { return newBuffer(f.conf) } -func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { +func (f *Formatter) Format(vcl *ast.VCL) io.Reader { buf := new(bytes.Buffer) for i, stmt := range vcl.Statements { @@ -64,7 +64,7 @@ func (f *Formatter) Format(vcl *ast.VCL) (io.Reader, error) { } } - return bytes.NewReader(buf.Bytes()), nil + return bytes.NewReader(buf.Bytes()) } func (f *Formatter) indent(level int) string { diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go index 57c1a1ad..70c74a9a 100644 --- a/formatter/formatter_test.go +++ b/formatter/formatter_test.go @@ -15,7 +15,7 @@ func assert(t *testing.T, input, expect string, conf *config.FormatConfig) strin IndentWidth: 2, IndentStyle: "space", TrailingCommentWidth: 2, - LineWidth: 80, + LineWidth: 120, } if conf != nil { c = conf @@ -25,11 +25,7 @@ func assert(t *testing.T, input, expect string, conf *config.FormatConfig) strin t.Errorf("Unexpected parser error: %s", err) return "" } - ret, err := New(c).Format(vcl) - if err != nil { - t.Errorf("Unexpected error returned: %s", err) - return "" - } + ret := New(c).Format(vcl) v, _ := ioutil.ReadAll(ret) if diff := cmp.Diff(string(v), expect); diff != "" { t.Errorf("Format result has diff: %s", diff) diff --git a/formatter/statement_format.go b/formatter/statement_format.go index a939055d..29844241 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -2,6 +2,7 @@ package formatter import ( "bytes" + "fmt" "strings" "github.com/ysugimoto/falco/ast" @@ -166,13 +167,24 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { var buf bytes.Buffer buf.WriteString(stmt.Keyword + " (") - chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, buf.Len()) + + // Condition expression chunk should be printed with multi-line + offset := buf.Len() + chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, offset) if strings.Contains(chunk, "\n") { - buf.WriteString("\n" + chunk + "\n") + buf.WriteString( + fmt.Sprintf( + "\n%s%s%s\n", + f.indent(stmt.Nest), + strings.Repeat(" ", offset), + chunk, + ), + ) + buf.WriteString(f.indent(stmt.Nest) + ") ") } else { - buf.WriteString(chunk) + buf.WriteString(chunk + ") ") } - buf.WriteString(") ") + buf.WriteString(f.formatBlockStatement(stmt.Consequence)) for _, a := range stmt.Another { // If leading comments exists, keyword should be placed with line-feed @@ -319,7 +331,7 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { if stmt.ReturnExpression != nil { prefix := " " suffix := "" - if f.conf.ReturnArgumentParenthesis { + if f.conf.ReturnStatementParenthesis { prefix = " (" suffix = ")" } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 87502c5e..3f816939 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -478,10 +478,10 @@ func TestFormatReturnStatement(t *testing.T) { } `, conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - TrailingCommentWidth: 2, - ReturnArgumentParenthesis: true, + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + ReturnStatementParenthesis: true, }, }, { @@ -506,10 +506,10 @@ func TestFormatReturnStatement(t *testing.T) { } `, conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - TrailingCommentWidth: 2, - ReturnArgumentParenthesis: false, + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + ReturnStatementParenthesis: false, }, }, } @@ -766,6 +766,30 @@ func TestFormatIfStatement(t *testing.T) { } `, }, + { + name: "chunked condition format", + input: `sub vcl_recv { + if (req.http.Header1 == "1" && req.http.Header2 == "2" && req.http.Header3 == "3" && req.http.Header4 == "4") { + set req.http.OK = "1"; + } +} +`, + expect: `sub vcl_recv { + if ( + req.http.Header1 == "1" && req.http.Header2 == "2" && + req.http.Header3 == "3" && req.http.Header4 == "4" + ) { + set req.http.OK = "1"; + } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 80, + }, + }, } for _, tt := range tests { diff --git a/resolver/file.go b/resolver/file.go index 9f34c578..248a2cba 100644 --- a/resolver/file.go +++ b/resolver/file.go @@ -57,7 +57,7 @@ func (f *FileResolver) Name() string { return "" } -func (f *FileResolver) getVCL(file string) (*VCL, error) { +func getVCL(file string) (*VCL, error) { if _, err := os.Stat(file); err != nil { return nil, err } @@ -80,7 +80,7 @@ func (f *FileResolver) getVCL(file string) (*VCL, error) { } func (f *FileResolver) MainVCL() (*VCL, error) { - return f.getVCL(f.main) + return getVCL(f.main) } func (f *FileResolver) Resolve(stmt *ast.IncludeStatement) (*VCL, error) { @@ -91,7 +91,7 @@ func (f *FileResolver) Resolve(stmt *ast.IncludeStatement) (*VCL, error) { // Find for each include paths for _, p := range f.includePaths { - if vcl, err := f.getVCL(filepath.Join(p, modulePathWithExtension)); err == nil { + if vcl, err := getVCL(filepath.Join(p, modulePathWithExtension)); err == nil { return vcl, nil } } diff --git a/resolver/glob.go b/resolver/glob.go new file mode 100644 index 00000000..b104f99c --- /dev/null +++ b/resolver/glob.go @@ -0,0 +1,50 @@ +package resolver + +import ( + "fmt" + "path/filepath" + + "github.com/pkg/errors" + "github.com/ysugimoto/falco/ast" +) + +// GlobResolver is glob pattern factory resolver, will be used for formatter +// Note that GlobResolver does not want to include paths +// because formatter does not need to resolve included files +type GlobResolver struct { + main string +} + +func NewGlobResolver(patterns ...string) ([]Resolver, error) { + var resolvers []Resolver + + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, errors.New(fmt.Sprintf(`Invalid glob pattern "%s" provided`, pattern)) + } + for i := range matches { + abs, err := filepath.Abs(matches[i]) + if err != nil { + return nil, errors.New(fmt.Sprintf(`Failed to get abosulte path: %s`, err.Error())) + } + resolvers = append(resolvers, &GlobResolver{ + main: abs, + }) + } + } + + return resolvers, nil +} + +func (g *GlobResolver) Name() string { + return "" +} + +func (g *GlobResolver) MainVCL() (*VCL, error) { + return getVCL(g.main) +} + +func (g *GlobResolver) Resolve(sttmt *ast.IncludeStatement) (*VCL, error) { + return nil, errors.New(fmt.Sprintf("GlobResolver does not support resolve VCL")) +} From 2b3ae83f3e8ff05cc7da6592501c49e53c7f0bcb Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 27 Mar 2024 23:35:46 +0900 Subject: [PATCH 08/22] linting --- cmd/falco/runner.go | 2 +- resolver/glob.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/falco/runner.go b/cmd/falco/runner.go index ee3ce7fd..a9f1cbfc 100644 --- a/cmd/falco/runner.go +++ b/cmd/falco/runner.go @@ -503,7 +503,7 @@ func (r *Runner) Format(rslv resolver.Resolver) error { var w io.Writer if r.config.Format.Overwrite { writeln(cyan, "Formatted %s.", main.Name) - fp, err := os.OpenFile(main.Name, os.O_TRUNC|os.O_WRONLY, 0644) + fp, err := os.OpenFile(main.Name, os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return errors.WithStack(err) } diff --git a/resolver/glob.go b/resolver/glob.go index b104f99c..b80301ae 100644 --- a/resolver/glob.go +++ b/resolver/glob.go @@ -46,5 +46,5 @@ func (g *GlobResolver) MainVCL() (*VCL, error) { } func (g *GlobResolver) Resolve(sttmt *ast.IncludeStatement) (*VCL, error) { - return nil, errors.New(fmt.Sprintf("GlobResolver does not support resolve VCL")) + return nil, errors.New("GlobResolver does not support resolving VCL") } From 8530335f09fecefe37f7a4cf553e357db24cc7ba Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Thu, 28 Mar 2024 20:58:43 +0900 Subject: [PATCH 09/22] keep empty lines --- ast/ast.go | 11 +++-- docs/formatter.md | 2 +- formatter/formatter.go | 10 ++++ formatter/statement_format.go | 3 ++ formatter/statement_format_test.go | 35 ++++++++++++++ parser/parser.go | 14 +++++- parser/parser_test.go | 74 ++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 7 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 909432ac..f8592e55 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -27,11 +27,12 @@ type Expression interface { // Meta struct of all nodes type Meta struct { - Token token.Token - Leading Comments - Trailing Comments - Infix Comments - Nest int + Token token.Token + Leading Comments + Trailing Comments + Infix Comments + Nest int + PreviousEmptyLines int } func (m *Meta) LeadingComment() string { diff --git a/docs/formatter.md b/docs/formatter.md index 87b7191a..0206e244 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -177,7 +177,7 @@ sub vcl_recv { --- -### Explicit Declaration Contact +### Explicit Declaration Concat **default: false** diff --git a/formatter/formatter.go b/formatter/formatter.go index 6abb1c43..ca5373fc 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -27,6 +27,9 @@ func (f *Formatter) Format(vcl *ast.VCL) io.Reader { buf := new(bytes.Buffer) for i, stmt := range vcl.Statements { + if i > 0 { + buf.WriteString(f.lineFeed(stmt.GetMeta())) + } buf.WriteString(f.formatComment(stmt.GetMeta().Leading, "\n", 0)) var formatted string trailingNode := stmt @@ -85,3 +88,10 @@ func (f *Formatter) trailing(trailing ast.Comments) string { c += f.formatComment(trailing, "", 0) return c } + +func (f *Formatter) lineFeed(meta *ast.Meta) string { + if meta.PreviousEmptyLines > 1 { + return "\n" + } + return "" +} diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 29844241..63ce66aa 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -111,6 +111,9 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { buf.WriteString("{\n") for i := range stmt.Statements { + if i > 0 { + buf.WriteString(f.lineFeed(stmt.Statements[i].GetMeta())) + } buf.WriteString(f.formatStatement(stmt.Statements[i])) buf.WriteString("\n") } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 3f816939..a153fdef 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -798,3 +798,38 @@ func TestFormatIfStatement(t *testing.T) { }) } } + +func TestFormatEmptyLines(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "format multiple empty lines to single line", + input: `sub vcl_recv { + +// Leading empty line inside block statement should be cut out +set req.http.Foo = "bar"; + + +// After second statement should be kept with single empty line +set req.http.Foo = "baz"; +}`, + expect: `sub vcl_recv { + // Leading empty line inside block statement should be cut out + set req.http.Foo = "bar"; + + // After second statement should be kept with single empty line + set req.http.Foo = "baz"; +} +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} diff --git a/parser/parser.go b/parser/parser.go index bf625211..4aea9377 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -78,12 +78,22 @@ func (p *Parser) nextToken() { func (p *Parser) readPeek() { leading := ast.Comments{} var prefixedLineFeed bool + var previousEmptyLines int for { t := p.l.NextToken() switch t.Type { case token.LF: prefixedLineFeed = true + // Count empty lines between the next token + for { + peek := p.l.PeekToken() + if peek.Type != token.LF { + break + } + previousEmptyLines++ + p.l.NextToken() + } continue case token.COMMENT: leading = append(leading, &ast.Comment{ @@ -97,7 +107,9 @@ func (p *Parser) readPeek() { case token.RIGHT_BRACE: p.level-- } - p.peekToken = ast.New(t, p.level, leading) + meta := ast.New(t, p.level, leading) + meta.PreviousEmptyLines = previousEmptyLines + p.peekToken = meta break } } diff --git a/parser/parser_test.go b/parser/parser_test.go index b60a58ed..95fe6b0c 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -275,3 +275,77 @@ sub /* subroutine ident leading */ vcl_recv /* subroutine block leading */ { } assert(t, vcl, expect) } + +func TestCountPreviousEmptyLines(t *testing.T) { + input := ` +sub vcl_recv { + + + set req.http.Foo = "bar"; +}` + expect := &ast.VCL{ + Statements: []ast.Statement{ + &ast.SubroutineDeclaration{ + Meta: &ast.Meta{ + Token: T, + Nest: 0, + PreviousEmptyLines: 0, + Leading: ast.Comments{}, + Infix: ast.Comments{}, + Trailing: ast.Comments{}, + }, + Name: &ast.Ident{ + Meta: &ast.Meta{ + Token: T, + Nest: 0, + PreviousEmptyLines: 0, + Leading: ast.Comments{}, + Infix: ast.Comments{}, + Trailing: ast.Comments{}, + }, + Value: "vcl_recv", + }, + Block: &ast.BlockStatement{ + Meta: &ast.Meta{ + Token: T, + Nest: 1, + PreviousEmptyLines: 0, + Leading: ast.Comments{}, + Infix: ast.Comments{}, + Trailing: ast.Comments{}, + }, + Statements: []ast.Statement{ + &ast.SetStatement{ + Meta: &ast.Meta{ + Token: T, + Nest: 1, + PreviousEmptyLines: 2, + Leading: ast.Comments{}, + Infix: ast.Comments{}, + Trailing: ast.Comments{}, + }, + Ident: &ast.Ident{ + Meta: ast.New(T, 1), + Value: "req.http.Foo", + }, + Operator: &ast.Operator{ + Meta: ast.New(T, 1), + Operator: "=", + }, + Value: &ast.String{ + Meta: ast.New(T, 1), + Value: "bar", + }, + }, + }, + }, + }, + }, + } + vcl, err := New(lexer.NewFromString(input)).ParseVCL() + if err != nil { + t.Errorf("%+v", err) + } + assert(t, vcl, expect) + +} From 33dd59dcd5b4f5f50e84b7f4b9d0ab21ddc586b7 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Sun, 31 Mar 2024 06:48:39 +0900 Subject: [PATCH 10/22] implement line stack --- formatter/declaration_line.go | 84 ++++++++++++++++++++++++++++++ formatter/declaration_line_test.go | 71 +++++++++++++++++++++++++ formatter/statement_line.go | 45 ++++++++++++++++ formatter/statement_line_test.go | 27 ++++++++++ 4 files changed, 227 insertions(+) create mode 100644 formatter/declaration_line.go create mode 100644 formatter/declaration_line_test.go create mode 100644 formatter/statement_line.go create mode 100644 formatter/statement_line_test.go diff --git a/formatter/declaration_line.go b/formatter/declaration_line.go new file mode 100644 index 00000000..7e95dd54 --- /dev/null +++ b/formatter/declaration_line.go @@ -0,0 +1,84 @@ +package formatter + +import ( + "sort" +) + +// DeclarationType represents formatted line name - can present on root scope +type DeclarationType int + +const ( + Import DeclarationType = iota + 1 + Include + Acl + Backend + Director + Table + Penaltybox + Ratecounter + Subroutine +) + +// Key is subroutine name, value is sort order +var fastlyReservedSubroutineNames = map[string]int{ + "vcl_recv": 0, + "vcl_hash": 1, + "vcl_hit": 2, + "vcl_miss": 3, + "vcl_pass": 4, + "vcl_fetch": 5, + "vcl_error": 6, + "vcl_deliver": 7, + "vcl_log": 8, +} + +type Declaration struct { + Type DeclarationType + Name string + Buffer string +} + +type Declarations []Declaration + +func (d Declarations) Sort() Declarations { + // step.1 divide fastly subroutines, user defined subourines and others + var fastlySubroutines Declarations + var userDefinedSubroutines Declarations + var others Declarations + + for i := range d { + if d[i].Type == Subroutine { + if _, ok := fastlyReservedSubroutineNames[d[i].Name]; ok { + fastlySubroutines = append(fastlySubroutines, d[i]) + } else { + userDefinedSubroutines = append(userDefinedSubroutines, d[i]) + } + } else { + others = append(others, d[i]) + } + } + + // step.2 sort by name for subroutine, type by other declarations + sort.Slice(others, func(i, j int) bool { + if others[i].Type == others[j].Type { + return others[i].Name < others[j].Name + } + return others[i].Type < others[j].Type + }) + sort.Slice(fastlySubroutines, func(i, j int) bool { + a := fastlyReservedSubroutineNames[fastlySubroutines[i].Name] + b := fastlyReservedSubroutineNames[fastlySubroutines[j].Name] + return a < b + }) + sort.Slice(userDefinedSubroutines, func(i, j int) bool { + return userDefinedSubroutines[i].Name < userDefinedSubroutines[j].Name + }) + + // Combine slices as others -> fastlySubroutines -> userDefinedSubroutines + var sorted Declarations + sorted = append(sorted, others...) + sorted = append(sorted, fastlySubroutines...) + sorted = append(sorted, userDefinedSubroutines...) + + return sorted +} diff --git a/formatter/declaration_line_test.go b/formatter/declaration_line_test.go new file mode 100644 index 00000000..79fcb628 --- /dev/null +++ b/formatter/declaration_line_test.go @@ -0,0 +1,71 @@ +package formatter + +import ( + "math/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestSortDeclarations(t *testing.T) { + decls := Declarations{ + Declaration{Type: Import, Name: "import1"}, + Declaration{Type: Include, Name: "include1"}, + Declaration{Type: Acl, Name: "acl"}, + Declaration{Type: Backend, Name: "backend1"}, + Declaration{Type: Director, Name: "director"}, + Declaration{Type: Ratecounter, Name: "ratecounter"}, + Declaration{Type: Table, Name: "table1"}, + Declaration{Type: Backend, Name: "backend2"}, + Declaration{Type: Table, Name: "table2"}, + Declaration{Type: Subroutine, Name: "vcl_recv"}, + Declaration{Type: Subroutine, Name: "vcl_hash"}, + Declaration{Type: Penaltybox, Name: "penaltybox"}, + Declaration{Type: Subroutine, Name: "vcl_miss"}, + Declaration{Type: Subroutine, Name: "vcl_pass"}, + Declaration{Type: Subroutine, Name: "vcl_fetch"}, + Declaration{Type: Subroutine, Name: "vcl_hit"}, + Declaration{Type: Subroutine, Name: "vcl_deliver"}, + Declaration{Type: Subroutine, Name: "vcl_log"}, + Declaration{Type: Subroutine, Name: "user_defined1"}, + Declaration{Type: Subroutine, Name: "vcl_error"}, + Declaration{Type: Subroutine, Name: "user_defined2"}, + } + + // Random shuffle + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(decls), func(i, j int) { + decls[i], decls[j] = decls[j], decls[i] + }) + + sorted := decls.Sort() + + expects := Declarations{ + Declaration{Type: Import, Name: "import1"}, + Declaration{Type: Include, Name: "include1"}, + Declaration{Type: Acl, Name: "acl"}, + Declaration{Type: Backend, Name: "backend1"}, + Declaration{Type: Backend, Name: "backend2"}, + Declaration{Type: Director, Name: "director"}, + Declaration{Type: Table, Name: "table1"}, + Declaration{Type: Table, Name: "table2"}, + Declaration{Type: Penaltybox, Name: "penaltybox"}, + Declaration{Type: Ratecounter, Name: "ratecounter"}, + Declaration{Type: Subroutine, Name: "vcl_recv"}, + Declaration{Type: Subroutine, Name: "vcl_hash"}, + Declaration{Type: Subroutine, Name: "vcl_hit"}, + Declaration{Type: Subroutine, Name: "vcl_miss"}, + Declaration{Type: Subroutine, Name: "vcl_pass"}, + Declaration{Type: Subroutine, Name: "vcl_fetch"}, + Declaration{Type: Subroutine, Name: "vcl_error"}, + Declaration{Type: Subroutine, Name: "vcl_deliver"}, + Declaration{Type: Subroutine, Name: "vcl_log"}, + Declaration{Type: Subroutine, Name: "user_defined1"}, + Declaration{Type: Subroutine, Name: "user_defined2"}, + } + + if diff := cmp.Diff(sorted, expects); diff != "" { + t.Errorf("Sorted result mismatch, diff=%s", diff) + } +} diff --git a/formatter/statement_line.go b/formatter/statement_line.go new file mode 100644 index 00000000..fbaccde2 --- /dev/null +++ b/formatter/statement_line.go @@ -0,0 +1,45 @@ +package formatter + +import "fmt" + +type Line struct { + Buffer string + Trailing string +} + +type Alignable interface { + Align() +} + +type Lines []Line + +func (l Lines) Align() { + var maxLength int + + for i := range l { + if len(l[i].Buffer) > maxLength { + maxLength = len(l[i].Buffer) + } + } + + // Alignment + format := fmt.Sprintf("%%-%ds", maxLength) + + for i := range l { + l[i].Buffer = fmt.Sprintf(format, l[i].Buffer) + } +} + +var _ Alignable = (*Lines)(nil) + +type GroupedLines struct { + Lines []Lines +} + +func (g *GroupedLines) Align() { + for i := range g.Lines { + g.Lines[i].Align() + } +} + +var _ Alignable = (*GroupedLines)(nil) diff --git a/formatter/statement_line_test.go b/formatter/statement_line_test.go new file mode 100644 index 00000000..bdd9ff6a --- /dev/null +++ b/formatter/statement_line_test.go @@ -0,0 +1,27 @@ +package formatter + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStatementsAlign(t *testing.T) { + lines := Lines{ + {Buffer: `"192.0.2.0"/24;`, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12";`}, + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + } + + lines.Align() + + expects := Lines{ + {Buffer: `"192.0.2.0"/24; `, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12"; `}, + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + } + + if diff := cmp.Diff(lines, expects); diff != "" { + t.Errorf("Align result mismatch, diff=%s", diff) + } +} From 2ce1cdd98c0538a6a262d8072c95174c79a4d1c5 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Sun, 31 Mar 2024 19:54:56 +0900 Subject: [PATCH 11/22] chore: add Leading comment --- formatter/statement_line.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/formatter/statement_line.go b/formatter/statement_line.go index fbaccde2..f71a977a 100644 --- a/formatter/statement_line.go +++ b/formatter/statement_line.go @@ -4,6 +4,8 @@ import "fmt" type Line struct { Buffer string + Operator string + Leading string Trailing string } From aa0688aecc5fe19f53fa9e10aa7d89db437f8883 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Tue, 2 Apr 2024 04:15:29 +0900 Subject: [PATCH 12/22] add line struct and slice --- config/config.go | 2 + formatter/declaration_format.go | 315 +++++++++++++++++---------- formatter/declaration_format_test.go | 139 +++++++++++- formatter/declaration_line.go | 84 ------- formatter/declaration_line_test.go | 71 ------ formatter/formatter.go | 68 +++--- formatter/lines.go | 256 ++++++++++++++++++++++ formatter/lines_test.go | 127 +++++++++++ formatter/statement_format.go | 147 ++++++++----- formatter/statement_format_test.go | 1 + formatter/statement_line.go | 47 ---- formatter/statement_line_test.go | 27 --- 12 files changed, 856 insertions(+), 428 deletions(-) delete mode 100644 formatter/declaration_line.go delete mode 100644 formatter/declaration_line_test.go create mode 100644 formatter/lines.go create mode 100644 formatter/lines_test.go delete mode 100644 formatter/statement_line.go delete mode 100644 formatter/statement_line_test.go diff --git a/config/config.go b/config/config.go index b89f6d90..f8c6c432 100644 --- a/config/config.go +++ b/config/config.go @@ -68,6 +68,8 @@ type FormatConfig struct { AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` ElseIf bool `yaml:"else_if" default:"false"` ReturnStatementParenthesis bool `yaml:"return_statement_parenthesis" default:"true"` + SortDeclaration bool `yaml:"sort_declaration" defaul:"false"` + AlignTrailingComment bool `yaml:"align_trailing_comment" default:"false"` } type Config struct { diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index 97c661da..de7d0252 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -8,15 +8,16 @@ import ( "github.com/ysugimoto/falco/ast" ) -func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { - var buf bytes.Buffer - - buf.WriteString("acl " + decl.Name.Value + " {\n") - for i, cidr := range decl.CIDRs { - if i > 0 { - buf.WriteString(f.lineFeed(cidr.GetMeta())) +func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration { + group := &GroupedLines{} + lines := DelclarationPropertyLines{} + + for _, cidr := range decl.CIDRs { + if cidr.GetMeta().PreviousEmptyLines > 0 { + group.Lines = append(group.Lines, lines) + lines = DelclarationPropertyLines{} } - buf.WriteString(f.formatComment(cidr.Leading, "\n", 1)) + var buf bytes.Buffer buf.WriteString(f.indent(1)) if cidr.Inverse != nil && cidr.Inverse.Value { buf.WriteString("!") @@ -25,20 +26,40 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) string { if cidr.Mask != nil { buf.WriteString("/" + cidr.Mask.String()) } - buf.WriteString(";") - buf.WriteString(f.trailing(cidr.Trailing)) - buf.WriteString("\n") + lines = append(lines, &DelclarationPropertyLine{ + Leading: f.formatComment(cidr.Leading, "\n", 1), + Trailing: f.trailing(cidr.Trailing), + Key: buf.String(), + EndCharacter: ";", + }) } + + // Append remaining lines + if len(lines) > 0 { + group.Lines = append(group.Lines, lines) + } + + if f.conf.AlignTrailingComment { + group.Align() + } + + var buf bytes.Buffer + buf.WriteString("acl " + decl.Name.Value + " {\n") + buf.WriteString(group.String()) if len(decl.Infix) > 0 { buf.WriteString(f.indent(1)) buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - return buf.String() + return &Declaration{ + Type: Acl, + Name: decl.Name.Value, + Buffer: buf.String(), + } } -func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) string { +func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) *Declaration { var buf bytes.Buffer buf.WriteString("backend " + decl.Name.Value + " {\n") @@ -49,101 +70,140 @@ func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) strin } buf.WriteString("}") - return buf.String() + return &Declaration{ + Type: Backend, + Name: decl.Name.Value, + Buffer: buf.String(), + } } func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLevel int) string { - var buf bytes.Buffer - var maxPropLength int + group := &GroupedLines{} + lines := DelclarationPropertyLines{} - if f.conf.SortDeclarationProperty { - sort.Slice(props, func(i, j int) bool { - if props[i].Key.String() == "probe" { - return false + for _, prop := range props { + if prop.GetMeta().PreviousEmptyLines > 0 { + if f.conf.AlignDeclarationProperty { + lines.AlignKey() } - return props[i].Key.String() < props[j].Key.String() - }) - } - - for i := range props { - if len(props[i].Key.String()) > maxPropLength { - maxPropLength = len(props[i].Key.String()) + if f.conf.SortDeclarationProperty { + lines.Sort() + } + group.Lines = append(group.Lines, lines) + lines = DelclarationPropertyLines{} } - } - for i, prop := range props { - if i > 0 { - buf.WriteString(f.lineFeed(prop.GetMeta())) + line := &DelclarationPropertyLine{ + Leading: f.formatComment(prop.Leading, "\n", nestLevel), + Trailing: f.trailing(prop.Trailing), + Key: f.indent(nestLevel) + "." + prop.Key.String(), + Operator: " = ", } - buf.WriteString(f.formatComment(prop.Leading, "\n", nestLevel)) - buf.WriteString(f.indent(nestLevel)) - var left string - if f.conf.AlignDeclarationProperty { - format := fmt.Sprintf("%%-%ds", maxPropLength) - left = fmt.Sprintf("."+format+" = ", prop.Key.String()) - } else { - left = fmt.Sprintf(".%s = ", prop.Key.String()) - } - buf.WriteString(left) if po, ok := prop.Value.(*ast.BackendProbeObject); ok { - buf.WriteString("{\n") - buf.WriteString(f.formatBackendProperties(po.Values, nestLevel+1)) - buf.WriteString(f.indent(nestLevel) + "}") + line.Value = "{\n" + line.Value += f.formatBackendProperties(po.Values, nestLevel+1) + line.Value += f.indent(nestLevel) + "}" + line.isObject = true } else { - buf.WriteString(f.formatExpression(prop.Value).ChunkedString(prop.Nest, len(left))) - buf.WriteString(";") + line.Value = f.formatExpression(prop.Value).ChunkedString(prop.Nest, len(line.Key)) + line.EndCharacter = ";" } - buf.WriteString(f.trailing(prop.Trailing)) - buf.WriteString("\n") - } - return buf.String() -} -func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) string { - var buf bytes.Buffer + lines = append(lines, line) + } - buf.WriteString("director " + decl.Name.Value + " " + decl.DirectorType.Value + " {\n") - for i, prop := range decl.Properties { - if i > 0 { - buf.WriteString(f.lineFeed(prop.GetMeta())) + // Append remaining lines + if len(lines) > 0 { + if f.conf.AlignDeclarationProperty { + lines.AlignKey() } - buf.WriteString(f.indent(1)) - buf.WriteString(f.formatDirectorProperty(prop.(*ast.DirectorBackendObject))) - buf.WriteString("\n") + if f.conf.SortDeclarationProperty { + lines.Sort() + } + group.Lines = append(group.Lines, lines) } - if len(decl.Infix) > 0 { - buf.WriteString(f.indent(1)) - buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) + + if f.conf.AlignTrailingComment { + group.Align() } - buf.WriteString("}") - return buf.String() + return group.String() } -func (f *Formatter) formatDirectorProperty(prop *ast.DirectorBackendObject) string { - var buf bytes.Buffer +func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *Declaration { + group := &GroupedLines{} + lines := DelclarationPropertyLines{} - if f.conf.SortDeclarationProperty { - sort.Slice(prop.Values, func(i, j int) bool { - return prop.Values[i].Key.Value < prop.Values[j].Key.Value - }) + for _, prop := range decl.Properties { + if prop.GetMeta().PreviousEmptyLines > 0 { + if f.conf.AlignDeclarationProperty { + lines.AlignKey() + } + if f.conf.SortDeclarationProperty { + lines.Sort() + } + group.Lines = append(group.Lines, lines) + lines = DelclarationPropertyLines{} + } + line := &DelclarationPropertyLine{ + Leading: f.formatComment(prop.GetMeta().Leading, "\n", 1), + Trailing: f.trailing(prop.GetMeta().Trailing), + Key: f.indent(1), + } + switch t := prop.(type) { + case *ast.DirectorBackendObject: + line.Key += "{ " + if f.conf.SortDeclarationProperty { + sort.Slice(t.Values, func(i, j int) bool { + return t.Values[i].Key.Value < t.Values[j].Key.Value + }) + } + for _, v := range t.Values { + line.Key += fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value)) + } + line.Key += "}" + line.isObject = true + case *ast.DirectorProperty: + line.Key += "." + t.Key.Value + line.Operator = " = " + line.Value = t.Value.String() + line.EndCharacter = ";" + } + lines = append(lines, line) } - buf.WriteString(f.formatComment(prop.Leading, "\n", 0)) - buf.WriteString("{ ") - for i, v := range prop.Values { - if i > 0 { - buf.WriteString(f.lineFeed(prop.GetMeta())) + // Append remaining lines + if len(lines) > 0 { + if f.conf.AlignDeclarationProperty { + lines.AlignKey() + } + if f.conf.SortDeclarationProperty { + lines.Sort() } - buf.WriteString(fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value))) + group.Lines = append(group.Lines, lines) + } + + if f.conf.AlignTrailingComment { + group.Align() + } + + var buf bytes.Buffer + buf.WriteString("director " + decl.Name.Value + " " + decl.DirectorType.Value + " {\n") + buf.WriteString(group.String()) + if len(decl.Infix) > 0 { + buf.WriteString(f.indent(1)) + buf.WriteString(f.formatComment(decl.Infix, "\n", 0)) } buf.WriteString("}") - buf.WriteString(f.trailing(prop.Trailing)) - return buf.String() + return &Declaration{ + Type: Director, + Name: decl.Name.Value, + Buffer: buf.String(), + } } -func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { +func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) *Declaration { var buf bytes.Buffer buf.WriteString("table " + decl.Name.Value) @@ -158,81 +218,106 @@ func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) string { } buf.WriteString("}") - return buf.String() + return &Declaration{ + Type: Table, + Name: decl.Name.Value, + Buffer: buf.String(), + } } func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { - var buf bytes.Buffer - var maxPropLength int + group := &GroupedLines{} + lines := DelclarationPropertyLines{} - if f.conf.SortDeclarationProperty { - sort.Slice(props, func(i, j int) bool { - return props[i].Key.Value < props[j].Key.Value - }) - } - - for i := range props { - if len(props[i].Key.String()) > maxPropLength { - maxPropLength = len(props[i].Key.String()) + for _, prop := range props { + if prop.Meta.PreviousEmptyLines > 0 { + if f.conf.AlignDeclarationProperty { + lines.AlignKey() + } + if f.conf.SortDeclarationProperty { + lines.Sort() + } + group.Lines = append(group.Lines, lines) + lines = DelclarationPropertyLines{} } + line := &DelclarationPropertyLine{ + Leading: f.formatComment(prop.Meta.Leading, "\n", 1), + Trailing: f.trailing(prop.Meta.Trailing), + Operator: ": ", + Key: f.indent(1) + f.formatString(prop.Key), + Value: f.formatExpression(prop.Value).String(), + EndCharacter: ",", + } + lines = append(lines, line) } - for i, prop := range props { - if i > 0 { - buf.WriteString(f.lineFeed(prop.Meta)) - } - buf.WriteString(f.formatComment(prop.Leading, "\n", 0)) - buf.WriteString(f.indent(1)) + // Append remaining lines + if len(lines) > 0 { if f.conf.AlignDeclarationProperty { - format := fmt.Sprintf("%%-%ds", maxPropLength) - buf.WriteString(fmt.Sprintf(format+": ", f.formatString(prop.Key))) - } else { - buf.WriteString(fmt.Sprintf("%s: ", f.formatString(prop.Key))) + lines.AlignKey() } - buf.WriteString(f.formatExpression(prop.Value).String()) - buf.WriteString(",") - buf.WriteString(f.trailing(prop.Trailing)) - buf.WriteString("\n") + if f.conf.SortDeclarationProperty { + lines.Sort() + } + group.Lines = append(group.Lines, lines) + } + + if f.conf.AlignTrailingComment { + group.Align() } - return buf.String() + return group.String() } -func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) string { +func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) *Declaration { var buf bytes.Buffer buf.WriteString("penaltybox " + decl.Name.Value) - buf.WriteString(" {\n") + buf.WriteString(" {") // penaltybox does not have properties if len(decl.Block.Infix) > 0 { + buf.WriteString("\n") buf.WriteString(f.indent(1)) buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - return buf.String() + return &Declaration{ + Type: Penaltybox, + Name: decl.Name.Value, + Buffer: buf.String(), + } } -func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) string { +func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) *Declaration { var buf bytes.Buffer buf.WriteString("ratecounter " + decl.Name.Value) - buf.WriteString(" {\n") + buf.WriteString(" {") // ratecounter does not have properties if len(decl.Block.Infix) > 0 { + buf.WriteString("\n") buf.WriteString(f.indent(1)) buf.WriteString(f.formatComment(decl.Block.Infix, "\n", 0)) } buf.WriteString("}") - return buf.String() + return &Declaration{ + Type: Ratecounter, + Name: decl.Name.Value, + Buffer: buf.String(), + } } -func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) string { +func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) *Declaration { var buf bytes.Buffer buf.WriteString("sub " + decl.Name.Value + " ") buf.WriteString(f.formatBlockStatement(decl.Block)) - return buf.String() + return &Declaration{ + Type: Subroutine, + Name: decl.Name.Value, + Buffer: buf.String(), + } } diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index c5811f19..9e9b3065 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -45,6 +45,31 @@ func TestAclDeclarationFormat(t *testing.T) { } // trailing `, }, + { + name: "align trailing comment", + input: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; // some comment + + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // some comment + // The ending + } // trailing`, + expect: `acl name { + "192.0.2.0"/24; // some comment + !"192.0.2.12"; // some comment + + "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // some comment + // The ending +} // trailing +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 120, + AlignTrailingComment: true, + }, + }, } for _, tt := range tests { @@ -172,6 +197,47 @@ func TestBackendDeclarationFormat(t *testing.T) { LineWidth: 80, }, }, + { + name: "sorted and align properties and comments", + input: `backend example { + .connect_timeout = 1s; // some comment + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; // some comment + .ssl = true; + .probe = { + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; + } + }`, + expect: `backend example { + .between_bytes_timeout = 30s; // some comment + .connect_timeout = 1s; // some comment + .dynamic = true; + .first_byte_timeout = 30s; + .host = "example.com"; + .max_connections = 500; + .port = "443"; + .ssl = true; + .probe = { + .dummy = true; + .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AlignDeclarationProperty: true, + SortDeclarationProperty: true, + TrailingCommentWidth: 2, + LineWidth: 120, + AlignTrailingComment: true, + }, + }, } for _, tt := range tests { @@ -220,7 +286,34 @@ func TestDirectorDeclarationFormat(t *testing.T) { IndentStyle: "space", SortDeclarationProperty: true, TrailingCommentWidth: 2, - LineWidth: 80, + LineWidth: 120, + }, + }, + { + name: "sort properties and align comment", + input: `director example random { + .retries = 1; // some comment + .quorum = 1; // some comment + { .weight=1; .backend=F_backend1; } + { .weight=1; .backend=F_backend2; } + { .weight=1; .backend=F_backend3; } + }`, + expect: `director example random { + .quorum = 1; // some comment + .retries = 1; // some comment + { .backend = F_backend1; .weight = 1; } + { .backend = F_backend2; .weight = 1; } + { .backend = F_backend3; .weight = 1; } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + SortDeclarationProperty: true, + TrailingCommentWidth: 2, + LineWidth: 120, + AlignTrailingComment: true, + AlignDeclarationProperty: true, }, }, } @@ -289,7 +382,7 @@ func TestTableDeclarationFormat(t *testing.T) { }, }, { - name: "alignment properties", + name: "align properties", input: `table routing_table BACKEND { "a.example.com":F_backendA, "bb.example.com":F_backendB, @@ -309,6 +402,28 @@ func TestTableDeclarationFormat(t *testing.T) { LineWidth: 80, }, }, + { + name: "alig properties and comments", + input: `table routing_table BACKEND { + "a.example.com":F_backendA, + "bb.example.com":F_backendBB, // some comment + "ccc.example.com":F_backendCCC, // some comment + }`, + expect: `table routing_table BACKEND { + "a.example.com" : F_backendA, + "bb.example.com" : F_backendBB, // some comment + "ccc.example.com": F_backendCCC, // some comment +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + AlignDeclarationProperty: true, + TrailingCommentWidth: 2, + LineWidth: 120, + AlignTrailingComment: true, + }, + }, } for _, tt := range tests { @@ -391,6 +506,26 @@ sub vcl_recv { } // subroutine trailing comment `, }, + { + name: "align statements comment inside subroutine", + input: ` +sub vcl_recv { + set req.http.Foo = "bar"; // some comment + set req.htto.Lorem = "ipsum"; // some comment +}`, + expect: `sub vcl_recv { + set req.http.Foo = "bar"; // some comment + set req.htto.Lorem = "ipsum"; // some comment +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 120, + AlignTrailingComment: true, + }, + }, } for _, tt := range tests { diff --git a/formatter/declaration_line.go b/formatter/declaration_line.go deleted file mode 100644 index 7e95dd54..00000000 --- a/formatter/declaration_line.go +++ /dev/null @@ -1,84 +0,0 @@ -package formatter - -import ( - "sort" -) - -// DeclarationType represents formatted line name - can present on root scope -type DeclarationType int - -const ( - Import DeclarationType = iota + 1 - Include - Acl - Backend - Director - Table - Penaltybox - Ratecounter - Subroutine -) - -// Key is subroutine name, value is sort order -var fastlyReservedSubroutineNames = map[string]int{ - "vcl_recv": 0, - "vcl_hash": 1, - "vcl_hit": 2, - "vcl_miss": 3, - "vcl_pass": 4, - "vcl_fetch": 5, - "vcl_error": 6, - "vcl_deliver": 7, - "vcl_log": 8, -} - -type Declaration struct { - Type DeclarationType - Name string - Buffer string -} - -type Declarations []Declaration - -func (d Declarations) Sort() Declarations { - // step.1 divide fastly subroutines, user defined subourines and others - var fastlySubroutines Declarations - var userDefinedSubroutines Declarations - var others Declarations - - for i := range d { - if d[i].Type == Subroutine { - if _, ok := fastlyReservedSubroutineNames[d[i].Name]; ok { - fastlySubroutines = append(fastlySubroutines, d[i]) - } else { - userDefinedSubroutines = append(userDefinedSubroutines, d[i]) - } - } else { - others = append(others, d[i]) - } - } - - // step.2 sort by name for subroutine, type by other declarations - sort.Slice(others, func(i, j int) bool { - if others[i].Type == others[j].Type { - return others[i].Name < others[j].Name - } - return others[i].Type < others[j].Type - }) - sort.Slice(fastlySubroutines, func(i, j int) bool { - a := fastlyReservedSubroutineNames[fastlySubroutines[i].Name] - b := fastlyReservedSubroutineNames[fastlySubroutines[j].Name] - return a < b - }) - sort.Slice(userDefinedSubroutines, func(i, j int) bool { - return userDefinedSubroutines[i].Name < userDefinedSubroutines[j].Name - }) - - // Combine slices as others -> fastlySubroutines -> userDefinedSubroutines - var sorted Declarations - sorted = append(sorted, others...) - sorted = append(sorted, fastlySubroutines...) - sorted = append(sorted, userDefinedSubroutines...) - - return sorted -} diff --git a/formatter/declaration_line_test.go b/formatter/declaration_line_test.go deleted file mode 100644 index 79fcb628..00000000 --- a/formatter/declaration_line_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package formatter - -import ( - "math/rand" - "testing" - "time" - - "github.com/google/go-cmp/cmp" -) - -func TestSortDeclarations(t *testing.T) { - decls := Declarations{ - Declaration{Type: Import, Name: "import1"}, - Declaration{Type: Include, Name: "include1"}, - Declaration{Type: Acl, Name: "acl"}, - Declaration{Type: Backend, Name: "backend1"}, - Declaration{Type: Director, Name: "director"}, - Declaration{Type: Ratecounter, Name: "ratecounter"}, - Declaration{Type: Table, Name: "table1"}, - Declaration{Type: Backend, Name: "backend2"}, - Declaration{Type: Table, Name: "table2"}, - Declaration{Type: Subroutine, Name: "vcl_recv"}, - Declaration{Type: Subroutine, Name: "vcl_hash"}, - Declaration{Type: Penaltybox, Name: "penaltybox"}, - Declaration{Type: Subroutine, Name: "vcl_miss"}, - Declaration{Type: Subroutine, Name: "vcl_pass"}, - Declaration{Type: Subroutine, Name: "vcl_fetch"}, - Declaration{Type: Subroutine, Name: "vcl_hit"}, - Declaration{Type: Subroutine, Name: "vcl_deliver"}, - Declaration{Type: Subroutine, Name: "vcl_log"}, - Declaration{Type: Subroutine, Name: "user_defined1"}, - Declaration{Type: Subroutine, Name: "vcl_error"}, - Declaration{Type: Subroutine, Name: "user_defined2"}, - } - - // Random shuffle - r := rand.New(rand.NewSource(time.Now().UnixNano())) - r.Shuffle(len(decls), func(i, j int) { - decls[i], decls[j] = decls[j], decls[i] - }) - - sorted := decls.Sort() - - expects := Declarations{ - Declaration{Type: Import, Name: "import1"}, - Declaration{Type: Include, Name: "include1"}, - Declaration{Type: Acl, Name: "acl"}, - Declaration{Type: Backend, Name: "backend1"}, - Declaration{Type: Backend, Name: "backend2"}, - Declaration{Type: Director, Name: "director"}, - Declaration{Type: Table, Name: "table1"}, - Declaration{Type: Table, Name: "table2"}, - Declaration{Type: Penaltybox, Name: "penaltybox"}, - Declaration{Type: Ratecounter, Name: "ratecounter"}, - Declaration{Type: Subroutine, Name: "vcl_recv"}, - Declaration{Type: Subroutine, Name: "vcl_hash"}, - Declaration{Type: Subroutine, Name: "vcl_hit"}, - Declaration{Type: Subroutine, Name: "vcl_miss"}, - Declaration{Type: Subroutine, Name: "vcl_pass"}, - Declaration{Type: Subroutine, Name: "vcl_fetch"}, - Declaration{Type: Subroutine, Name: "vcl_error"}, - Declaration{Type: Subroutine, Name: "vcl_deliver"}, - Declaration{Type: Subroutine, Name: "vcl_log"}, - Declaration{Type: Subroutine, Name: "user_defined1"}, - Declaration{Type: Subroutine, Name: "user_defined2"}, - } - - if diff := cmp.Diff(sorted, expects); diff != "" { - t.Errorf("Sorted result mismatch, diff=%s", diff) - } -} diff --git a/formatter/formatter.go b/formatter/formatter.go index 8080c93d..a1ed8a19 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -2,6 +2,7 @@ package formatter import ( "bytes" + "fmt" "io" "strings" @@ -24,45 +25,65 @@ func (f *Formatter) chunkBuffer() *ChunkBuffer { } func (f *Formatter) Format(vcl *ast.VCL) io.Reader { - buf := new(bytes.Buffer) - - for i, stmt := range vcl.Statements { - if i > 0 { - buf.WriteString(f.lineFeed(stmt.GetMeta())) - } - buf.WriteString(f.formatComment(stmt.GetMeta().Leading, "\n", 0)) - var formatted string + decls := Declarations{} + for _, stmt := range vcl.Statements { + var decl *Declaration trailingNode := stmt switch t := stmt.(type) { case *ast.ImportStatement: - formatted = f.formatImportStatement(t) + decl = &Declaration{ + Type: Import, + Buffer: f.formatImportStatement(t), + } case *ast.IncludeStatement: - formatted = f.formatIncludeStatement(t) + decl = &Declaration{ + Type: Include, + Buffer: f.formatIncludeStatement(t), + } case *ast.AclDeclaration: - formatted = f.formatAclDeclaration(t) + decl = f.formatAclDeclaration(t) case *ast.BackendDeclaration: - formatted = f.formatBackendDeclaration(t) + decl = f.formatBackendDeclaration(t) case *ast.DirectorDeclaration: - formatted = f.formatDirectorDeclaration(t) + decl = f.formatDirectorDeclaration(t) case *ast.TableDeclaration: - formatted = f.formatTableDeclaration(t) + decl = f.formatTableDeclaration(t) // penaltybox, ratecounter, and subroutine have trailing comment on the block statement case *ast.PenaltyboxDeclaration: - formatted = f.formatPenaltyboxDeclaration(t) + decl = f.formatPenaltyboxDeclaration(t) trailingNode = t.Block case *ast.RatecounterDeclaration: - formatted = f.formatRatecounterDeclaration(t) + decl = f.formatRatecounterDeclaration(t) trailingNode = t.Block case *ast.SubroutineDeclaration: - formatted = f.formatSubroutineDeclaration(t) + decl = f.formatSubroutineDeclaration(t) trailingNode = t.Block + default: + return nil } - buf.WriteString(formatted) - buf.WriteString(f.trailing(trailingNode.GetMeta().Trailing)) + + decl.Buffer = fmt.Sprintf( + "%s%s%s", + f.formatComment(stmt.GetMeta().Leading, "\n", 0), + decl.Buffer, + f.trailing(trailingNode.GetMeta().Trailing), + ) + decls = append(decls, decl) + } + + if f.conf.SortDeclaration { + decls.Sort() + } + + // output + var buf bytes.Buffer + + for i, decl := range decls { + buf.WriteString(decl.Buffer) buf.WriteString("\n") - if i != len(vcl.Statements)-1 { + if i != len(decls)-1 { buf.WriteString("\n") } } @@ -88,10 +109,3 @@ func (f *Formatter) trailing(trailing ast.Comments) string { c += f.formatComment(trailing, "", 0) return c } - -func (f *Formatter) lineFeed(meta *ast.Meta) string { - if meta.PreviousEmptyLines > 0 { - return "\n" - } - return "" -} diff --git a/formatter/lines.go b/formatter/lines.go new file mode 100644 index 00000000..8789c7ef --- /dev/null +++ b/formatter/lines.go @@ -0,0 +1,256 @@ +package formatter + +import ( + "bytes" + "fmt" + "sort" +) + +type Line struct { + Buffer string + Leading string + Trailing string +} + +func (l Line) String() string { + var buf bytes.Buffer + + buf.WriteString(l.Leading) + buf.WriteString(l.Buffer) + buf.WriteString(l.Trailing) + + return buf.String() +} + +type Alignable interface { + Align() + String() string +} + +type Lines []*Line + +func (l Lines) Align() { + var maxLength int + + for i := range l { + if len(l[i].Buffer) > maxLength { + maxLength = len(l[i].Buffer) + } + } + + // Alignment + format := fmt.Sprintf("%%-%ds", maxLength) + + for i := range l { + l[i].Buffer = fmt.Sprintf(format, l[i].Buffer) + } +} + +func (l Lines) String() string { + var buf bytes.Buffer + + for i := range l { + buf.WriteString(l[i].String()) + buf.WriteString("\n") + } + + return buf.String() +} + +var _ Alignable = (*Lines)(nil) + +type DelclarationPropertyLine struct { + Key string + Operator string + Value string + Leading string + Trailing string + Offset int + isObject bool + EndCharacter string +} + +func (l DelclarationPropertyLine) isIgnoreTarget() bool { + // Ignore objective property + return l.isObject +} + +type DelclarationPropertyLines []*DelclarationPropertyLine + +func (l DelclarationPropertyLines) AlignKey() { + var maxLength int + + for i := range l { + if l[i].isIgnoreTarget() { + continue + } + if len(l[i].Key) > maxLength { + maxLength = len(l[i].Key) + } + } + // Alignment + format := fmt.Sprintf("%%-%ds", maxLength) + + for i := range l { + l[i].Key = fmt.Sprintf(format, l[i].Key) + } +} + +func (l DelclarationPropertyLines) Align() { + var maxLength int + + for i := range l { + if l[i].isIgnoreTarget() { + continue + } + v := l[i].Key + if l[i].Value != "" { + v += l[i].Operator + l[i].Value + } + v += l[i].EndCharacter + if len(v) > maxLength { + maxLength = len(v) + } + } + + for i := range l { + l[i].Offset = maxLength + } +} + +func (l DelclarationPropertyLines) Sort() { + sort.Slice(l, func(i, j int) bool { + if l[i].isObject { + return false + } + return l[i].Key < l[j].Key + }) +} + +func (l DelclarationPropertyLines) String() string { + var buf bytes.Buffer + + for i := range l { + buf.WriteString(l[i].Leading) + v := l[i].Key + if l[i].Value != "" { + v += l[i].Operator + l[i].Value + } + v += l[i].EndCharacter + if l[i].Offset > 0 && l[i].Trailing != "" { + format := fmt.Sprintf("%%-%ds", l[i].Offset) + v = fmt.Sprintf(format, v) + } + buf.WriteString(v) + buf.WriteString(l[i].Trailing) + buf.WriteString("\n") + } + + return buf.String() +} + +var _ Alignable = (*Lines)(nil) + +type GroupedLines struct { + Lines []Alignable +} + +func (g *GroupedLines) Align() { + for i := range g.Lines { + g.Lines[i].Align() + } +} + +func (g *GroupedLines) String() string { + var buf bytes.Buffer + + for i := range g.Lines { + buf.WriteString(g.Lines[i].String()) + if i != len(g.Lines)-1 { + buf.WriteString("\n") + } + } + + return buf.String() +} + +var _ Alignable = (*GroupedLines)(nil) + +// DeclarationType represents formatted line name - can present on root scope +type DeclarationType int + +const ( + Import DeclarationType = iota + 1 + Include + Acl + Backend + Director + Table + Penaltybox + Ratecounter + Subroutine +) + +// Key is subroutine name, value is sort order +var fastlyReservedSubroutineNames = map[string]int{ + "vcl_recv": 0, + "vcl_hash": 1, + "vcl_hit": 2, + "vcl_miss": 3, + "vcl_pass": 4, + "vcl_fetch": 5, + "vcl_error": 6, + "vcl_deliver": 7, + "vcl_log": 8, +} + +type Declaration struct { + Type DeclarationType + Name string + Buffer string +} + +type Declarations []*Declaration + +func (d Declarations) Sort() { + // step.1 divide fastly subroutines, user defined subourines and others + var fastlySubroutines Declarations + var userDefinedSubroutines Declarations + var others Declarations + + for i := range d { + if d[i].Type == Subroutine { + if _, ok := fastlyReservedSubroutineNames[d[i].Name]; ok { + fastlySubroutines = append(fastlySubroutines, d[i]) + } else { + userDefinedSubroutines = append(userDefinedSubroutines, d[i]) + } + } else { + others = append(others, d[i]) + } + } + + // step.2 sort by name for subroutine, type for other declarations + sort.Slice(others, func(i, j int) bool { + if others[i].Type == others[j].Type { + return others[i].Name < others[j].Name + } + return others[i].Type < others[j].Type + }) + sort.Slice(fastlySubroutines, func(i, j int) bool { + a := fastlyReservedSubroutineNames[fastlySubroutines[i].Name] + b := fastlyReservedSubroutineNames[fastlySubroutines[j].Name] + return a < b + }) + sort.Slice(userDefinedSubroutines, func(i, j int) bool { + return userDefinedSubroutines[i].Name < userDefinedSubroutines[j].Name + }) + + // Combine slices as others -> fastlySubroutines -> userDefinedSubroutines order + var sorted Declarations + sorted = append(sorted, others...) + sorted = append(sorted, fastlySubroutines...) + sorted = append(sorted, userDefinedSubroutines...) + + copy(d, sorted) +} diff --git a/formatter/lines_test.go b/formatter/lines_test.go new file mode 100644 index 00000000..cc655f42 --- /dev/null +++ b/formatter/lines_test.go @@ -0,0 +1,127 @@ +package formatter + +import ( + "math/rand" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestAclCIDRAlign(t *testing.T) { + tests := []struct { + name string + lines Alignable + expect Alignable + }{ + { + name: "basic alignment", + lines: Lines{ + {Buffer: `"192.0.2.0"/24;`, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12";`}, + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + }, + expect: Lines{ + {Buffer: `"192.0.2.0"/24; `, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12"; `}, + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + }, + }, + { + name: "grouped lines alignment", + lines: &GroupedLines{ + Lines: []Alignable{ + Lines{ + {Buffer: `"192.0.2.0"/24;`, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12";`}, + }, + Lines{ + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + }, + }, + }, + expect: &GroupedLines{ + Lines: []Alignable{ + Lines{ + {Buffer: `"192.0.2.0"/24;`, Trailing: "// some comment"}, + {Buffer: `!"192.0.2.12"; `}, + }, + Lines{ + {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.lines.Align() + if diff := cmp.Diff(tt.lines, tt.expect); diff != "" { + t.Errorf("Align result mismatch, diff=%s", diff) + } + }) + } +} + +func TestSortDeclarations(t *testing.T) { + decls := Declarations{ + {Type: Import, Name: "import1"}, + {Type: Include, Name: "include1"}, + {Type: Acl, Name: "acl"}, + {Type: Backend, Name: "backend1"}, + {Type: Director, Name: "director"}, + {Type: Ratecounter, Name: "ratecounter"}, + {Type: Table, Name: "table1"}, + {Type: Backend, Name: "backend2"}, + {Type: Table, Name: "table2"}, + {Type: Subroutine, Name: "vcl_recv"}, + {Type: Subroutine, Name: "vcl_hash"}, + {Type: Penaltybox, Name: "penaltybox"}, + {Type: Subroutine, Name: "vcl_miss"}, + {Type: Subroutine, Name: "vcl_pass"}, + {Type: Subroutine, Name: "vcl_fetch"}, + {Type: Subroutine, Name: "vcl_hit"}, + {Type: Subroutine, Name: "vcl_deliver"}, + {Type: Subroutine, Name: "vcl_log"}, + {Type: Subroutine, Name: "user_defined1"}, + {Type: Subroutine, Name: "vcl_error"}, + {Type: Subroutine, Name: "user_defined2"}, + } + + // Random shuffle + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(decls), func(i, j int) { + decls[i], decls[j] = decls[j], decls[i] + }) + + decls.Sort() + + expects := Declarations{ + {Type: Import, Name: "import1"}, + {Type: Include, Name: "include1"}, + {Type: Acl, Name: "acl"}, + {Type: Backend, Name: "backend1"}, + {Type: Backend, Name: "backend2"}, + {Type: Director, Name: "director"}, + {Type: Table, Name: "table1"}, + {Type: Table, Name: "table2"}, + {Type: Penaltybox, Name: "penaltybox"}, + {Type: Ratecounter, Name: "ratecounter"}, + {Type: Subroutine, Name: "vcl_recv"}, + {Type: Subroutine, Name: "vcl_hash"}, + {Type: Subroutine, Name: "vcl_hit"}, + {Type: Subroutine, Name: "vcl_miss"}, + {Type: Subroutine, Name: "vcl_pass"}, + {Type: Subroutine, Name: "vcl_fetch"}, + {Type: Subroutine, Name: "vcl_error"}, + {Type: Subroutine, Name: "vcl_deliver"}, + {Type: Subroutine, Name: "vcl_log"}, + {Type: Subroutine, Name: "user_defined1"}, + {Type: Subroutine, Name: "user_defined2"}, + } + + if diff := cmp.Diff(decls, expects); diff != "" { + t.Errorf("Sorted result mismatch, diff=%s", diff) + } +} diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 63ce66aa..7e49dedc 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -8,66 +8,65 @@ import ( "github.com/ysugimoto/falco/ast" ) -func (f *Formatter) formatStatement(stmt ast.Statement) string { - var buf bytes.Buffer - +func (f *Formatter) formatStatement(stmt ast.Statement) *Line { if block, ok := stmt.(*ast.BlockStatement); ok { - // need subtract 1 because LEFT_BRACE is unnested - buf.WriteString(f.formatComment(block.Leading, "\n", block.Nest-1)) - buf.WriteString(f.indent(block.Nest - 1)) - buf.WriteString(f.formatBlockStatement(block)) - buf.WriteString(f.trailing(block.Trailing)) - return buf.String() + return &Line{ + // need subtract 1 because LEFT_BRACE is unnested + Leading: f.formatComment(block.Leading, "\n", block.Nest-1), + Trailing: f.trailing(block.Trailing), + Buffer: f.indent(block.Nest-1) + f.formatBlockStatement(block), + } } - buf.WriteString(f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest)) - buf.WriteString(f.indent(stmt.GetMeta().Nest)) + line := &Line{ + Leading: f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest), + Buffer: f.indent(stmt.GetMeta().Nest), + } - var formatted string trailingNode := stmt switch t := stmt.(type) { case *ast.ImportStatement: - formatted = f.formatImportStatement(t) + line.Buffer += f.formatImportStatement(t) case *ast.IncludeStatement: - formatted = f.formatIncludeStatement(t) + line.Buffer += f.formatIncludeStatement(t) case *ast.DeclareStatement: - formatted = f.formatDeclareStatement(t) + line.Buffer += f.formatDeclareStatement(t) case *ast.SetStatement: - formatted = f.formatSetStatement(t) + line.Buffer += f.formatSetStatement(t) case *ast.UnsetStatement: - formatted = f.formatUnsetStatement(t) + line.Buffer += f.formatUnsetStatement(t) case *ast.RemoveStatement: - formatted = f.formatRemoveStatement(t) + line.Buffer += f.formatRemoveStatement(t) case *ast.SwitchStatement: - formatted = f.formatSwitchStatement(t) + line.Buffer += f.formatSwitchStatement(t) case *ast.RestartStatement: - formatted = f.formatRestartStatement() + line.Buffer += f.formatRestartStatement() case *ast.EsiStatement: - formatted = f.formatEsiStatement() + line.Buffer += f.formatEsiStatement() case *ast.AddStatement: - formatted = f.formatAddStatement(t) + line.Buffer += f.formatAddStatement(t) case *ast.CallStatement: - formatted = f.formatCallStatement(t) + line.Buffer += f.formatCallStatement(t) case *ast.ErrorStatement: - formatted = f.formatErrorStatement(t) + line.Buffer += f.formatErrorStatement(t) case *ast.LogStatement: - formatted = f.formatLogStatement(t) + line.Buffer += f.formatLogStatement(t) case *ast.ReturnStatement: - formatted = f.formatReturnStatement(t) + line.Buffer += f.formatReturnStatement(t) case *ast.SyntheticStatement: - formatted = f.formatSyntheticStatement(t) + line.Buffer += f.formatSyntheticStatement(t) case *ast.SyntheticBase64Statement: - formatted = f.formatSyntheticBase64Statement(t) + line.Buffer += f.formatSyntheticBase64Statement(t) case *ast.GotoStatement: - formatted = f.formatGotoStatement(t) + line.Buffer += f.formatGotoStatement(t) case *ast.GotoDestinationStatement: - formatted = f.formatGotoDestinationStatement(t) + line.Buffer += f.formatGotoDestinationStatement(t) case *ast.FunctionCallStatement: - formatted = f.formatFunctionCallStatement(t) + line.Buffer += f.formatFunctionCallStatement(t) // On if statement, trailing comment node depends on its declarations case *ast.IfStatement: - formatted = f.formatIfStatement(t) + line.Buffer += f.formatIfStatement(t) switch { case t.Alternative != nil: // When "else" statament exists, trailing comment will be on it @@ -80,10 +79,9 @@ func (f *Formatter) formatStatement(stmt ast.Statement) string { trailingNode = t.Consequence } } - buf.WriteString(formatted) - buf.WriteString(f.trailing(trailingNode.GetMeta().Trailing)) + line.Trailing = f.trailing(trailingNode.GetMeta().Trailing) - return buf.String() + return line } func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { @@ -107,16 +105,27 @@ func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { } func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { - var buf bytes.Buffer + group := &GroupedLines{} + lines := Lines{} - buf.WriteString("{\n") - for i := range stmt.Statements { - if i > 0 { - buf.WriteString(f.lineFeed(stmt.Statements[i].GetMeta())) + for _, s := range stmt.Statements { + if s.GetMeta().PreviousEmptyLines > 0 { + group.Lines = append(group.Lines, lines) + lines = Lines{} } - buf.WriteString(f.formatStatement(stmt.Statements[i])) - buf.WriteString("\n") + lines = append(lines, f.formatStatement(s)) + } + + if len(lines) > 0 { + group.Lines = append(group.Lines, lines) + } + if f.conf.AlignTrailingComment { + group.Align() } + + var buf bytes.Buffer + buf.WriteString("{\n") + buf.WriteString(group.String()) if len(stmt.Infix) > 0 { buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) } @@ -246,19 +255,8 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { } else { buf.WriteString("default:\n") } - for _, s := range c.Statements { - if _, ok := s.(*ast.BreakStatement); ok { - buf.WriteString(f.indent(c.Meta.Nest + 1)) - buf.WriteString("break;") - } else { - buf.WriteString(f.formatStatement(s)) - } - buf.WriteString("\n") - } - if c.Fallthrough { - buf.WriteString(f.indent(c.Meta.Nest + 1)) - buf.WriteString("fallthrough;\n") - } + buf.WriteString(f.formatCaseSectionStatements(c)) + buf.WriteString("\n") } if len(stmt.Infix) > 0 { buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) @@ -269,6 +267,45 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { return buf.String() } +func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { + group := &GroupedLines{} + lines := Lines{} + + for _, stmt := range cs.Statements { + if stmt.GetMeta().PreviousEmptyLines > 0 { + group.Lines = append(group.Lines, lines) + lines = Lines{} + } + line := &Line{ + Leading: f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest), + Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", stmt.GetMeta().Nest), + } + if _, ok := stmt.(*ast.BreakStatement); ok { + line.Buffer = f.indent(stmt.GetMeta().Nest+1) + "break;" + } else { + line.Buffer = f.formatStatement(stmt).String() + } + lines = append(lines, line) + } + + if len(lines) > 0 { + group.Lines = append(group.Lines, lines) + } + + if f.conf.AlignTrailingComment { + group.Align() + } + + var buf bytes.Buffer + buf.WriteString(group.String()) + if cs.Fallthrough { + buf.WriteString(f.indent(cs.Meta.Nest + 1)) + buf.WriteString("fallthrough;") + } + + return buf.String() +} + func (f *Formatter) formatRestartStatement() string { var buf bytes.Buffer diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index a153fdef..14ec6139 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -818,6 +818,7 @@ set req.http.Foo = "bar"; set req.http.Foo = "baz"; }`, expect: `sub vcl_recv { + // Leading empty line inside block statement should be cut out set req.http.Foo = "bar"; diff --git a/formatter/statement_line.go b/formatter/statement_line.go deleted file mode 100644 index f71a977a..00000000 --- a/formatter/statement_line.go +++ /dev/null @@ -1,47 +0,0 @@ -package formatter - -import "fmt" - -type Line struct { - Buffer string - Operator string - Leading string - Trailing string -} - -type Alignable interface { - Align() -} - -type Lines []Line - -func (l Lines) Align() { - var maxLength int - - for i := range l { - if len(l[i].Buffer) > maxLength { - maxLength = len(l[i].Buffer) - } - } - - // Alignment - format := fmt.Sprintf("%%-%ds", maxLength) - - for i := range l { - l[i].Buffer = fmt.Sprintf(format, l[i].Buffer) - } -} - -var _ Alignable = (*Lines)(nil) - -type GroupedLines struct { - Lines []Lines -} - -func (g *GroupedLines) Align() { - for i := range g.Lines { - g.Lines[i].Align() - } -} - -var _ Alignable = (*GroupedLines)(nil) diff --git a/formatter/statement_line_test.go b/formatter/statement_line_test.go deleted file mode 100644 index bdd9ff6a..00000000 --- a/formatter/statement_line_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package formatter - -import ( - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestStatementsAlign(t *testing.T) { - lines := Lines{ - {Buffer: `"192.0.2.0"/24;`, Trailing: "// some comment"}, - {Buffer: `!"192.0.2.12";`}, - {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, - } - - lines.Align() - - expects := Lines{ - {Buffer: `"192.0.2.0"/24; `, Trailing: "// some comment"}, - {Buffer: `!"192.0.2.12"; `}, - {Buffer: `"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff";`}, - } - - if diff := cmp.Diff(lines, expects); diff != "" { - t.Errorf("Align result mismatch, diff=%s", diff) - } -} From d4e81949f5b1f1be3c8c89f9e73aabebf85b6639 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 3 Apr 2024 01:37:41 +0900 Subject: [PATCH 13/22] formatted as expect for my production vcl --- ast/comment.go | 3 +- config/config.go | 10 ++- formatter/chunk_buffer.go | 7 ++- formatter/comment_format.go | 21 ------- formatter/formatter.go | 45 ++++++++++++-- formatter/helper.go | 38 ++++++++++++ formatter/helper_test.go | 97 ++++++++++++++++++++++++++++++ formatter/statement_format.go | 43 +++++++------ formatter/statement_format_test.go | 61 ++++++++++++++++++- parser/parser.go | 8 ++- 10 files changed, 281 insertions(+), 52 deletions(-) delete mode 100644 formatter/comment_format.go create mode 100644 formatter/helper.go create mode 100644 formatter/helper_test.go diff --git a/ast/comment.go b/ast/comment.go index a3443f4f..11e7e52e 100644 --- a/ast/comment.go +++ b/ast/comment.go @@ -23,7 +23,8 @@ type Comment struct { // If this flag is false, it should be treated as trailing comment // because the comment presents on the same line. // Otherwise, this flag is true, it should be the leading comment for a next token. - PrefixedLineFeed bool + PrefixedLineFeed bool + PreviousEmptyLines int } func (c *Comment) String() string { diff --git a/config/config.go b/config/config.go index f8c6c432..6ef6e2bb 100644 --- a/config/config.go +++ b/config/config.go @@ -12,9 +12,13 @@ var ( configurationFiles = []string{".falco.yaml", ".falco.yml"} ) +// Formatting value constants const ( - IndentStyleSpace = "space" - IndentStyleTab = "tab" + IndentStyleSpace = "space" + IndentStyleTab = "tab" + CommentStyleNone = "none" + CommentStyleSlash = "slash" + CommentStyleSharp = "sharp" ) type OverrideBackend struct { @@ -67,9 +71,11 @@ type FormatConfig struct { SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` ElseIf bool `yaml:"else_if" default:"false"` + AlwaysNextLineElseIf bool `yaml:"always_next_line_else_if" default:"false"` ReturnStatementParenthesis bool `yaml:"return_statement_parenthesis" default:"true"` SortDeclaration bool `yaml:"sort_declaration" defaul:"false"` AlignTrailingComment bool `yaml:"align_trailing_comment" default:"false"` + CommentStyle string `yaml:"comment_style" default:"none"` } type Config struct { diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 0d471f80..f03e0187 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -32,6 +32,11 @@ func (c *ChunkBuffer) String() string { } func (c *ChunkBuffer) ChunkedString(level, offset int) string { + // If LineWidth configuration is disabled with -1 value, simply joins strings. + if c.conf.LineWidth < 0 { + return c.String() + } + var buf bytes.Buffer count := offset + level*c.conf.IndentWidth @@ -51,7 +56,7 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { count += len(b) } - return buf.String() + return strings.TrimSpace(buf.String()) } func (c *ChunkBuffer) indent(level int) string { diff --git a/formatter/comment_format.go b/formatter/comment_format.go deleted file mode 100644 index ee5282e4..00000000 --- a/formatter/comment_format.go +++ /dev/null @@ -1,21 +0,0 @@ -package formatter - -import ( - "bytes" - - "github.com/ysugimoto/falco/ast" -) - -func (f *Formatter) formatComment(comments ast.Comments, sep string, level int) string { - if len(comments) == 0 { - return "" - } - var buf bytes.Buffer - - for i := range comments { - buf.WriteString(f.indent(level) + comments[i].String()) - buf.WriteString(sep) - } - - return buf.String() -} diff --git a/formatter/formatter.go b/formatter/formatter.go index a1ed8a19..dce27b60 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -64,9 +64,15 @@ func (f *Formatter) Format(vcl *ast.VCL) io.Reader { return nil } + var lf string + if stmt.GetMeta().PreviousEmptyLines > 0 { + lf = "\n" + } + decl.Buffer = fmt.Sprintf( - "%s%s%s", + "%s%s%s%s", f.formatComment(stmt.GetMeta().Leading, "\n", 0), + lf, decl.Buffer, f.trailing(trailingNode.GetMeta().Trailing), ) @@ -81,12 +87,15 @@ func (f *Formatter) Format(vcl *ast.VCL) io.Reader { var buf bytes.Buffer for i, decl := range decls { - buf.WriteString(decl.Buffer) - buf.WriteString("\n") - if i != len(decls)-1 { + if i > 0 { buf.WriteString("\n") + if decl.Type != Import && decl.Type != Include { + buf.WriteString("\n") + } } + buf.WriteString(decl.Buffer) } + buf.WriteString("\n") return bytes.NewReader(buf.Bytes()) } @@ -109,3 +118,31 @@ func (f *Formatter) trailing(trailing ast.Comments) string { c += f.formatComment(trailing, "", 0) return c } + +func (f *Formatter) formatComment(comments ast.Comments, sep string, level int) string { + if len(comments) == 0 { + return "" + } + + var buf bytes.Buffer + + for i := range comments { + if comments[i].PreviousEmptyLines > 0 { + buf.WriteString("\n") + } + buf.WriteString(f.indent(level)) + switch f.conf.CommentStyle { + case config.CommentStyleSharp, config.CommentStyleSlash: + r := '#' // default as sharp style comment + if f.conf.CommentStyle == config.CommentStyleSlash { + r = '/' + } + buf.WriteString(formatCommentCharacter(comments[i].String(), r)) + default: + buf.WriteString(comments[i].String()) + } + buf.WriteString(sep) + } + + return buf.String() +} diff --git a/formatter/helper.go b/formatter/helper.go new file mode 100644 index 00000000..888342fc --- /dev/null +++ b/formatter/helper.go @@ -0,0 +1,38 @@ +package formatter + +import "regexp" + +var multiLineFeedRegex = regexp.MustCompile(`\n{3,}`) +var replace = "\n\n" + +func trimMutipleLineFeeds(lines string) string { + return multiLineFeedRegex.ReplaceAllString(lines, replace) +} + +func formatCommentCharacter(comment string, char rune) string { + bs := []rune(comment) + // Sharp-style comment + switch bs[0] { + case '#': + for i := range bs { + if bs[i] != '#' { + break + } + bs[i] = char + } + // Slash-style comment + case '/': + // Check inline comment like /* ... */ and return without replacing if so + if len(bs) < 2 || bs[1] == '*' { + return string(bs) + } + for i := range bs { + if bs[i] != '/' { + break + } + bs[i] = char + } + } + + return string(bs) +} diff --git a/formatter/helper_test.go b/formatter/helper_test.go new file mode 100644 index 00000000..730ab4b1 --- /dev/null +++ b/formatter/helper_test.go @@ -0,0 +1,97 @@ +package formatter + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestTrimMultipleLineFeeds(t *testing.T) { + tests := []struct { + name string + input string + expect string + }{ + { + name: "trim multile line feed", + input: "\n\n\nfoo\n\nbar\n\n\nbaz", + expect: "\n\nfoo\n\nbar\n\nbaz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ret := trimMutipleLineFeeds(tt.input) + if diff := cmp.Diff(ret, tt.expect); diff != "" { + t.Errorf("result mismatch, diff=%s", diff) + } + }) + } +} + +func TestFormatCommentCharacter(t *testing.T) { + tests := []struct { + name string + input string + char rune + expect string + }{ + { + name: "slash-style comment to sharp-style ", + input: "// foo bar baz", + char: '#', + expect: "## foo bar baz", + }, + { + name: "slash-style comment to slash-style ", + input: "// foo bar baz", + char: '/', + expect: "// foo bar baz", + }, + { + name: "inline-style comment to sharp-style ", + input: "/* foo bar baz */", + char: '#', + expect: "/* foo bar baz */", + }, + { + name: "mixed-style comment to sharp-style ", + input: "//# foo bar baz", + char: '#', + expect: "### foo bar baz", + }, + { + name: "sharp-style comment to slash-style ", + input: "## foo bar baz", + char: '/', + expect: "// foo bar baz", + }, + { + name: "sharp-style comment to sharp-style ", + input: "## foo bar baz", + char: '#', + expect: "## foo bar baz", + }, + { + name: "inline-style comment to slash-style ", + input: "/* foo bar baz */", + char: '/', + expect: "/* foo bar baz */", + }, + { + name: "mixed-style comment to slash-style ", + input: "//# foo bar baz", + char: '/', + expect: "//# foo bar baz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ret := formatCommentCharacter(tt.input, tt.char) + if diff := cmp.Diff(ret, tt.expect); diff != "" { + t.Errorf("result mismatch, diff=%s", diff) + } + }) + } +} diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 7e49dedc..183b84ac 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -23,6 +23,11 @@ func (f *Formatter) formatStatement(stmt ast.Statement) *Line { Buffer: f.indent(stmt.GetMeta().Nest), } + // Some statement may have empty lines without leading comment + if stmt.GetMeta().PreviousEmptyLines > 0 { + line.Buffer = "\n" + f.indent(stmt.GetMeta().Nest) + } + trailingNode := stmt switch t := stmt.(type) { case *ast.ImportStatement: @@ -133,7 +138,7 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { buf.WriteString(f.indent(stmt.Meta.Nest - 1)) buf.WriteString("}") - return buf.String() + return trimMutipleLineFeeds(buf.String()) } func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { @@ -181,14 +186,12 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { buf.WriteString(stmt.Keyword + " (") // Condition expression chunk should be printed with multi-line - offset := buf.Len() - chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, offset) + chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, stmt.Nest*f.conf.IndentWidth) if strings.Contains(chunk, "\n") { buf.WriteString( fmt.Sprintf( - "\n%s%s%s\n", - f.indent(stmt.Nest), - strings.Repeat(" ", offset), + "\n%s%s\n", + f.indent(stmt.Nest+1), chunk, ), ) @@ -199,8 +202,8 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { buf.WriteString(f.formatBlockStatement(stmt.Consequence)) for _, a := range stmt.Another { - // If leading comments exists, keyword should be placed with line-feed - if len(a.Leading) > 0 { + // If leading comments exists of configuration is enabled, keyword should be placed with line-feed + if len(a.Leading) > 0 || f.conf.AlwaysNextLineElseIf { buf.WriteString("\n") buf.WriteString(f.formatComment(a.Leading, "\n", a.Nest)) buf.WriteString(f.indent(a.Nest)) @@ -213,14 +216,20 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { if f.conf.ElseIf { keyword = "else if" } - chunk := f.formatExpression(a.Condition).ChunkedString(a.Nest, len(keyword)+2) + chunk := f.formatExpression(a.Condition).ChunkedString(a.Nest, a.Nest*f.conf.IndentWidth) buf.WriteString(keyword + " (") if strings.Contains(chunk, "\n") { - buf.WriteString("\n" + chunk + "\n") + buf.WriteString( + fmt.Sprintf( + "\n%s%s\n", + f.indent(a.Nest+1), + chunk, + ), + ) + buf.WriteString(f.indent(stmt.Nest) + ") ") } else { - buf.WriteString(chunk) + buf.WriteString(chunk + ") ") } - buf.WriteString(") ") buf.WriteString(f.formatBlockStatement(a.Consequence)) } if stmt.Alternative != nil { @@ -256,10 +265,9 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { buf.WriteString("default:\n") } buf.WriteString(f.formatCaseSectionStatements(c)) - buf.WriteString("\n") } if len(stmt.Infix) > 0 { - buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest)) + buf.WriteString(f.formatComment(stmt.Infix, "\n", stmt.Meta.Nest+1)) } buf.WriteString(f.indent(stmt.Meta.Nest)) buf.WriteString("}") @@ -276,9 +284,10 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { group.Lines = append(group.Lines, lines) lines = Lines{} } + // need to plus 1 to nested indent because parser won't increase nest level line := &Line{ - Leading: f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest), - Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", stmt.GetMeta().Nest), + Leading: f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest+1), + Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", stmt.GetMeta().Nest+1), } if _, ok := stmt.(*ast.BreakStatement); ok { line.Buffer = f.indent(stmt.GetMeta().Nest+1) + "break;" @@ -303,7 +312,7 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { buf.WriteString("fallthrough;") } - return buf.String() + return trimMutipleLineFeeds(buf.String()) } func (f *Formatter) formatRestartStatement() string { diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 14ec6139..52181db5 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -776,8 +776,8 @@ func TestFormatIfStatement(t *testing.T) { `, expect: `sub vcl_recv { if ( - req.http.Header1 == "1" && req.http.Header2 == "2" && - req.http.Header3 == "3" && req.http.Header4 == "4" + req.http.Header1 == "1" && req.http.Header2 == "2" && + req.http.Header3 == "3" && req.http.Header4 == "4" ) { set req.http.OK = "1"; } @@ -787,7 +787,7 @@ func TestFormatIfStatement(t *testing.T) { IndentWidth: 2, IndentStyle: "space", TrailingCommentWidth: 2, - LineWidth: 80, + LineWidth: 70, }, }, } @@ -799,6 +799,61 @@ func TestFormatIfStatement(t *testing.T) { } } +func TestFormatSwitchStatement(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formatting with comments", + input: `sub vcl_recv { + // switch leading comment + switch(req.http.Foo) { + // first case comment + case "bar": + // break leading comment + break; + // second case comment + case "baz": + // break leading comment + break; + // default case comment + default: + break; + // switch infix comment + } // trailing comment +} +`, + expect: `sub vcl_recv { + // switch leading comment + switch (req.http.Foo) { + // first case comment + case "bar": + // break leading comment + break; + // second case comment + case "baz": + // break leading comment + break; + // default case comment + default: + break; + // switch infix comment + } // trailing comment +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + func TestFormatEmptyLines(t *testing.T) { tests := []struct { name string diff --git a/parser/parser.go b/parser/parser.go index 3a8f2945..9194b571 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -101,10 +101,12 @@ func (p *Parser) readPeek() { continue case token.COMMENT: leading = append(leading, &ast.Comment{ - Token: t, - Value: t.Literal, - PrefixedLineFeed: prefixedLineFeed, + Token: t, + Value: t.Literal, + PrefixedLineFeed: prefixedLineFeed, + PreviousEmptyLines: previousEmptyLines, }) + previousEmptyLines = 0 continue case token.LEFT_BRACE: p.level++ From 7db5deb9d0bb592844c8c4c809cfe1eb357dc076 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 3 Apr 2024 01:39:43 +0900 Subject: [PATCH 14/22] fix config test --- config/config_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/config_test.go b/config/config_test.go index b11bec53..e8ab8d7c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -74,6 +74,9 @@ func TestConfigFromCLI(t *testing.T) { AlignDeclarationProperty: false, ElseIf: false, ReturnStatementParenthesis: true, + SortDeclaration: false, + AlignTrailingComment: false, + CommentStyle: "none", }, OverrideBackends: make(map[string]*OverrideBackend), } From 7e03cb50b0610b6de9ac2a19e577f16f1837f0ee Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 3 Apr 2024 02:41:40 +0900 Subject: [PATCH 15/22] commenting, add should_use_unset config --- config/config.go | 1 + config/config_test.go | 1 + formatter/chunk_buffer.go | 18 ++++---- formatter/declaration_format.go | 11 +++++ formatter/errors.go | 31 -------------- formatter/expression_format.go | 11 ++++- formatter/formatter.go | 20 ++++++--- formatter/helper.go | 18 +++++++- formatter/lines.go | 73 ++++++++++++++++++++++----------- formatter/statement_format.go | 47 ++++++++++++++++++--- 10 files changed, 155 insertions(+), 76 deletions(-) delete mode 100644 formatter/errors.go diff --git a/config/config.go b/config/config.go index 6ef6e2bb..72aec8a5 100644 --- a/config/config.go +++ b/config/config.go @@ -76,6 +76,7 @@ type FormatConfig struct { SortDeclaration bool `yaml:"sort_declaration" defaul:"false"` AlignTrailingComment bool `yaml:"align_trailing_comment" default:"false"` CommentStyle string `yaml:"comment_style" default:"none"` + ShouldUseUnset bool `yaml:"should_use_unset" default:"false"` } type Config struct { diff --git a/config/config_test.go b/config/config_test.go index e8ab8d7c..faf251f5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -77,6 +77,7 @@ func TestConfigFromCLI(t *testing.T) { SortDeclaration: false, AlignTrailingComment: false, CommentStyle: "none", + ShouldUseUnset: false, }, OverrideBackends: make(map[string]*OverrideBackend), } diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index f03e0187..29614f52 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -7,11 +7,13 @@ import ( "github.com/ysugimoto/falco/config" ) +// ChunkBuffer struct reperesents limited-line chunked string from configration. type ChunkBuffer struct { chunks []string conf *config.FormatConfig } +// Create ChunkBuffer pointer func newBuffer(c *config.FormatConfig) *ChunkBuffer { return &ChunkBuffer{ chunks: []string{}, @@ -19,18 +21,22 @@ func newBuffer(c *config.FormatConfig) *ChunkBuffer { } } +// Merge buffers func (c *ChunkBuffer) Merge(nc *ChunkBuffer) { c.chunks = append(c.chunks, nc.chunks...) } +// Write string to buffer - same as bytes.Buffer func (c *ChunkBuffer) WriteString(s string) { c.chunks = append(c.chunks, s) } +// Get "No" chunked string func (c *ChunkBuffer) String() string { return strings.Join(c.chunks, "") } +// Calculate line-chunked strings func (c *ChunkBuffer) ChunkedString(level, offset int) string { // If LineWidth configuration is disabled with -1 value, simply joins strings. if c.conf.LineWidth < 0 { @@ -41,9 +47,10 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { count := offset + level*c.conf.IndentWidth for i, b := range c.chunks { + // If adding next expression overflows line-width limit, insert line-feed and adjust indent if count+len(b) > c.conf.LineWidth { buf.WriteString("\n") - buf.WriteString(c.indent(level)) + buf.WriteString(indent(c.conf, level)) if offset > 0 { buf.WriteString(c.offsetString(offset)) } @@ -59,14 +66,7 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { return strings.TrimSpace(buf.String()) } -func (c *ChunkBuffer) indent(level int) string { - ws := " " // default as whitespace - if c.conf.IndentStyle == config.IndentStyleTab { - ws = "\t" - } - return strings.Repeat(ws, level*c.conf.IndentWidth) -} - +// Padding offset string func (c *ChunkBuffer) offsetString(offset int) string { ws := " " // default as whitespace if c.conf.IndentStyle == config.IndentStyleTab { diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index de7d0252..4a5a59ba 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -8,6 +8,7 @@ import ( "github.com/ysugimoto/falco/ast" ) +// Format acl declaration func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration { group := &GroupedLines{} lines := DelclarationPropertyLines{} @@ -59,6 +60,7 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration } } +// Format backend declaration func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) *Declaration { var buf bytes.Buffer @@ -77,6 +79,7 @@ func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) *Decl } } +// Format backend properties with align property names, trailing comment if needed func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLevel int) string { group := &GroupedLines{} lines := DelclarationPropertyLines{} @@ -103,6 +106,7 @@ func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLe line.Value = "{\n" line.Value += f.formatBackendProperties(po.Values, nestLevel+1) line.Value += f.indent(nestLevel) + "}" + // probe property is object, semicolon is not needed line.isObject = true } else { line.Value = f.formatExpression(prop.Value).ChunkedString(prop.Nest, len(line.Key)) @@ -130,6 +134,7 @@ func (f *Formatter) formatBackendProperties(props []*ast.BackendProperty, nestLe return group.String() } +// Format director declaration func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *Declaration { group := &GroupedLines{} lines := DelclarationPropertyLines{} @@ -162,6 +167,7 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *De line.Key += fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value)) } line.Key += "}" + // Backend property is object, semicolon is not needed line.isObject = true case *ast.DirectorProperty: line.Key += "." + t.Key.Value @@ -203,6 +209,7 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *De } } +// Format table declaration func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) *Declaration { var buf bytes.Buffer @@ -225,6 +232,7 @@ func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) *Declarat } } +// Format table declaration func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { group := &GroupedLines{} lines := DelclarationPropertyLines{} @@ -269,6 +277,7 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { return group.String() } +// Format penaltybox delclaration func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) *Declaration { var buf bytes.Buffer @@ -289,6 +298,7 @@ func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) } } +// Format ratecounter delclaration func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) *Declaration { var buf bytes.Buffer @@ -309,6 +319,7 @@ func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaratio } } +// Format subroutine declaration func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) *Declaration { var buf bytes.Buffer diff --git a/formatter/errors.go b/formatter/errors.go deleted file mode 100644 index 137b0f3d..00000000 --- a/formatter/errors.go +++ /dev/null @@ -1,31 +0,0 @@ -package formatter - -import ( - "fmt" - - "github.com/ysugimoto/falco/token" -) - -type FormatError struct { - token token.Token - message string -} - -func (f *FormatError) Error() string { - return f.message -} - -// shorthand functions -func UnexpectedToken(actual token.Token, expects token.TokenType) *FormatError { - return &FormatError{ - token: actual, - message: fmt.Sprintf(`Unexpected token found. Expects "%s" but found "%s"`, expects, actual.Type), - } -} - -func UnexpectedEOF(actual token.Token) *FormatError { - return &FormatError{ - token: actual, - message: "Unexpected EOF", - } -} diff --git a/formatter/expression_format.go b/formatter/expression_format.go index 9f38cd4d..06af6805 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -7,6 +7,8 @@ import ( "github.com/ysugimoto/falco/ast" ) +// Format expressions. +// Note that expressions may be chunked by limited line characters. func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { buf := f.chunkBuffer() @@ -90,6 +92,7 @@ func (f *Formatter) formatRTime(expr *ast.RTime) string { // Combinated expressions +// Format prefix expression like "!foo" func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuffer { buf := f.chunkBuffer() @@ -99,6 +102,8 @@ func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuf return buf } +// This map is used for the expression can be chunked or not. +// Following operators must be printed on a single line, otherwise VCL will cause syntax error. var mustSingleOperators = map[string]struct{}{ "==": {}, "!=": {}, @@ -120,6 +125,7 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe } } + // If operator must be printed on a single line, add combinated string to ChunkBuffer if _, ok := mustSingleOperators[operator]; ok { buf.WriteString( fmt.Sprintf( @@ -130,7 +136,7 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe ), ) } else { - // Can split to newline + // Otherwise, the exprssion can be printed to newline buf.Merge(f.formatExpression(expr.Left)) if operator != "" { buf.WriteString(operator) @@ -140,6 +146,7 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe return buf } +// Format prefix expression like `if(req.http.Foo, "foo", "bar")` func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { var buf bytes.Buffer @@ -154,6 +161,7 @@ func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { return buf.String() } +// Format parenthesis surrounded expression like `(req.http.Foo)` func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkBuffer { buf := f.chunkBuffer() @@ -164,6 +172,7 @@ func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkB return buf } +// Format function call expression like `regsuball(req.http.Foo, ".*", "$1")` func (f *Formatter) formatFunctionCallExpression(expr *ast.FunctionCallExpression) string { var buf bytes.Buffer diff --git a/formatter/formatter.go b/formatter/formatter.go index dce27b60..c822fca1 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -10,22 +10,31 @@ import ( "github.com/ysugimoto/falco/config" ) +// Formatter is struct for formatting input VCL. type Formatter struct { + // Defined at config package, all built-in formatting rules includes with default values conf *config.FormatConfig } +// Create Formatter pointer func New(conf *config.FormatConfig) *Formatter { return &Formatter{ conf: conf, } } +// Create ChunkBuffer by passing provided configuration func (f *Formatter) chunkBuffer() *ChunkBuffer { return newBuffer(f.conf) } +// Do formatting. +// Note that argument of vcl must be an AST-ed tree that is created by parser package. +// It means parser should have all information about input VCL (comment, empty lines, etc...) +// And of course input VCL must have a valid syntax. func (f *Formatter) Format(vcl *ast.VCL) io.Reader { decls := Declarations{} + for _, stmt := range vcl.Statements { var decl *Declaration trailingNode := stmt @@ -79,11 +88,11 @@ func (f *Formatter) Format(vcl *ast.VCL) io.Reader { decls = append(decls, decl) } + // If all declarations should be sorted, do it if f.conf.SortDeclaration { decls.Sort() } - // output var buf bytes.Buffer for i, decl := range decls { @@ -100,14 +109,12 @@ func (f *Formatter) Format(vcl *ast.VCL) io.Reader { return bytes.NewReader(buf.Bytes()) } +// Calculate and crate ident strings from config (shorthand, without passing config) func (f *Formatter) indent(level int) string { - c := " " // default as whitespace - if f.conf.IndentStyle == config.IndentStyleTab { - c = "\t" - } - return strings.Repeat(c, level*f.conf.IndentWidth) + return indent(f.conf, level) } +// Calculate trailing comments if exists func (f *Formatter) trailing(trailing ast.Comments) string { var c string @@ -119,6 +126,7 @@ func (f *Formatter) trailing(trailing ast.Comments) string { return c } +// Format leading/infix/trailing comments func (f *Formatter) formatComment(comments ast.Comments, sep string, level int) string { if len(comments) == 0 { return "" diff --git a/formatter/helper.go b/formatter/helper.go index 888342fc..3ab55866 100644 --- a/formatter/helper.go +++ b/formatter/helper.go @@ -1,14 +1,30 @@ package formatter -import "regexp" +import ( + "regexp" + "strings" + + "github.com/ysugimoto/falco/config" +) var multiLineFeedRegex = regexp.MustCompile(`\n{3,}`) var replace = "\n\n" +// Replace over three line-feed characters to two characters func trimMutipleLineFeeds(lines string) string { return multiLineFeedRegex.ReplaceAllString(lines, replace) } +// Calculate indent strings from configuration +func indent(conf *config.FormatConfig, level int) string { + c := " " // default as whitespace + if conf.IndentStyle == config.IndentStyleTab { + c = "\t" + } + return strings.Repeat(c, level*conf.IndentWidth) +} + +// Format comment line (ignore inline comment) func formatCommentCharacter(comment string, char rune) string { bs := []rune(comment) // Sharp-style comment diff --git a/formatter/lines.go b/formatter/lines.go index 8789c7ef..835986c7 100644 --- a/formatter/lines.go +++ b/formatter/lines.go @@ -6,12 +6,20 @@ import ( "sort" ) +// Alignable is an interface that should implements Align() and String() method +type Alignable interface { + Align() + String() string +} + +// Line represents single-line string including leading/trailing comment string type Line struct { Buffer string Leading string Trailing string } +// Get Line string func (l Line) String() string { var buf bytes.Buffer @@ -22,13 +30,10 @@ func (l Line) String() string { return buf.String() } -type Alignable interface { - Align() - String() string -} - +// Type alias for slice of Line type Lines []*Line +// Implements Alignable interface func (l Lines) Align() { var maxLength int @@ -46,6 +51,7 @@ func (l Lines) Align() { } } +// Implements Alignable interface func (l Lines) String() string { var buf bytes.Buffer @@ -57,8 +63,11 @@ func (l Lines) String() string { return buf.String() } +// Check satisfieng Alignable interface var _ Alignable = (*Lines)(nil) +// DelclarationPropertyLine represents a single line of declaration properties. +// This struct is used for acl, backend, director, and table properties type DelclarationPropertyLine struct { Key string Operator string @@ -70,18 +79,17 @@ type DelclarationPropertyLine struct { EndCharacter string } -func (l DelclarationPropertyLine) isIgnoreTarget() bool { - // Ignore objective property - return l.isObject -} - +// Type alias for slice of DelclarationPropertyLine type DelclarationPropertyLines []*DelclarationPropertyLine +// Declaration property name will be aligned from configuration +// so need to implement AlignKey() method to do it func (l DelclarationPropertyLines) AlignKey() { var maxLength int for i := range l { - if l[i].isIgnoreTarget() { + // Ignore the alignment target for object (e.g director backend, probe property in backend) + if l[i].isObject { continue } if len(l[i].Key) > maxLength { @@ -96,11 +104,23 @@ func (l DelclarationPropertyLines) AlignKey() { } } +// DelclarationPropertyLines could be sorted by name alphabetically +func (l DelclarationPropertyLines) Sort() { + sort.Slice(l, func(i, j int) bool { + // Ignore sorting target for object (e.g director backend, probe property in backend) + if l[i].isObject { + return false + } + return l[i].Key < l[j].Key + }) +} + +// Implement Alignable interface func (l DelclarationPropertyLines) Align() { var maxLength int for i := range l { - if l[i].isIgnoreTarget() { + if l[i].isObject { continue } v := l[i].Key @@ -118,15 +138,7 @@ func (l DelclarationPropertyLines) Align() { } } -func (l DelclarationPropertyLines) Sort() { - sort.Slice(l, func(i, j int) bool { - if l[i].isObject { - return false - } - return l[i].Key < l[j].Key - }) -} - +// Implement Alignable interface func (l DelclarationPropertyLines) String() string { var buf bytes.Buffer @@ -149,18 +161,32 @@ func (l DelclarationPropertyLines) String() string { return buf.String() } -var _ Alignable = (*Lines)(nil) - +// Check satisfieng Alignable interface +var _ Alignable = (*DelclarationPropertyLines)(nil) + +// GroupedLines represents grouped lines +// "grouped" means that lines are separated by empty line, for example: +// +// set req.http.Foo = "bar"; // group 1 +// set req.http.Foo = "baz"; // group 1 +// +// set req.http.Bar = "bar"; // group 2 +// set req.http.Bar = "baz"; // group 2 +// +// These group should be aligned for each group. type GroupedLines struct { + // Accept Alignable interface in order to append Lines or DelclarationPropertyLines Lines []Alignable } +// GroupedLines also satisfies Alignable interface func (g *GroupedLines) Align() { for i := range g.Lines { g.Lines[i].Align() } } +// GroupedLines also satisfies Alignable interface func (g *GroupedLines) String() string { var buf bytes.Buffer @@ -174,6 +200,7 @@ func (g *GroupedLines) String() string { return buf.String() } +// Check satisfieng Alignable interface var _ Alignable = (*GroupedLines)(nil) // DeclarationType represents formatted line name - can present on root scope diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 183b84ac..f3e4cab0 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -8,6 +8,7 @@ import ( "github.com/ysugimoto/falco/ast" ) +// Format VCL statements func (f *Formatter) formatStatement(stmt ast.Statement) *Line { if block, ok := stmt.(*ast.BlockStatement); ok { return &Line{ @@ -89,6 +90,7 @@ func (f *Formatter) formatStatement(stmt ast.Statement) *Line { return line } +// Format import statement func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { var buf bytes.Buffer @@ -99,6 +101,7 @@ func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { return buf.String() } +// Format include statement func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { var buf bytes.Buffer @@ -109,6 +112,8 @@ func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { return buf.String() } +// Format block statement. +// This method will be called from subroutine, if, else-if formatting func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { group := &GroupedLines{} lines := Lines{} @@ -141,6 +146,7 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { return trimMutipleLineFeeds(buf.String()) } +// Format delclare local varialbe statement func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { var buf bytes.Buffer @@ -151,6 +157,7 @@ func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { return buf.String() } +// Format set statement func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { var buf bytes.Buffer @@ -162,6 +169,7 @@ func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { return buf.String() } +// Format unset statement func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { var buf bytes.Buffer @@ -171,21 +179,29 @@ func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { return buf.String() } +// Format remove statement. func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { var buf bytes.Buffer - buf.WriteString("remove " + stmt.Ident.Value) + // The "remove" statement is alias of "unset" statement, + // so it could replaced to unset by configuration + if f.conf.ShouldUseUnset { + buf.WriteString("unset " + stmt.Ident.Value) + } else { + buf.WriteString("remove " + stmt.Ident.Value) + } buf.WriteString(";") return buf.String() } +// Format if statement func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { var buf bytes.Buffer buf.WriteString(stmt.Keyword + " (") - // Condition expression chunk should be printed with multi-line + // Condition expression chunk string may be printed with multi-lines. chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, stmt.Nest*f.conf.IndentWidth) if strings.Contains(chunk, "\n") { buf.WriteString( @@ -201,14 +217,17 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { } buf.WriteString(f.formatBlockStatement(stmt.Consequence)) + + // else if, elseif, elsif for _, a := range stmt.Another { - // If leading comments exists of configuration is enabled, keyword should be placed with line-feed + // If leading comments exists or AlwaysNextLineElseIf configuration is enabled, + // The keyword should be printed on the next line. if len(a.Leading) > 0 || f.conf.AlwaysNextLineElseIf { buf.WriteString("\n") buf.WriteString(f.formatComment(a.Leading, "\n", a.Nest)) buf.WriteString(f.indent(a.Nest)) } else { - // Otherwise, write one whitespace characeter + // Otherwise, write with whitespace characeter buf.WriteString(" ") } @@ -232,8 +251,10 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { } buf.WriteString(f.formatBlockStatement(a.Consequence)) } + + // else if stmt.Alternative != nil { - if len(stmt.Alternative.Leading) > 0 { + if len(stmt.Alternative.Leading) > 0 || f.conf.AlwaysNextLineElseIf { buf.WriteString("\n") buf.WriteString(f.formatComment(stmt.Alternative.Leading, "\n", stmt.Alternative.Nest)) buf.WriteString(f.indent(stmt.Alternative.Nest)) @@ -247,6 +268,7 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { return buf.String() } +// Format switch statement func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { var buf bytes.Buffer @@ -275,6 +297,7 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { return buf.String() } +// Format case statement inside switch statement func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { group := &GroupedLines{} lines := Lines{} @@ -315,6 +338,7 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { return trimMutipleLineFeeds(buf.String()) } +// Format restart statement func (f *Formatter) formatRestartStatement() string { var buf bytes.Buffer @@ -323,6 +347,7 @@ func (f *Formatter) formatRestartStatement() string { return buf.String() } +// Format esi statement func (f *Formatter) formatEsiStatement() string { var buf bytes.Buffer @@ -331,6 +356,7 @@ func (f *Formatter) formatEsiStatement() string { return buf.String() } +// Format add statement func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { var buf bytes.Buffer @@ -342,6 +368,7 @@ func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { return buf.String() } +// Format call statement func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { var buf bytes.Buffer @@ -351,10 +378,12 @@ func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { return buf.String() } +// Fromat error statement func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { var buf bytes.Buffer buf.WriteString("error " + f.formatExpression(stmt.Code).String()) + // argument is arbitrary if stmt.Argument != nil { buf.WriteString(" " + f.formatExpression(stmt.Argument).String()) } @@ -363,6 +392,7 @@ func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { return buf.String() } +// Format log statement func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { var buf bytes.Buffer @@ -373,6 +403,7 @@ func (f *Formatter) formatLogStatement(stmt *ast.LogStatement) string { return buf.String() } +// Format return statement func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { var buf bytes.Buffer @@ -380,6 +411,7 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { if stmt.ReturnExpression != nil { prefix := " " suffix := "" + // If ReturnStatementParenthesis is enabled, must be surrounded by parenthesis if f.conf.ReturnStatementParenthesis { prefix = " (" suffix = ")" @@ -393,6 +425,7 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { return buf.String() } +// Format synthetic statement func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) string { var buf bytes.Buffer @@ -403,6 +436,7 @@ func (f *Formatter) formatSyntheticStatement(stmt *ast.SyntheticStatement) strin return buf.String() } +// Format synthetic.base64 statement func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Statement) string { var buf bytes.Buffer @@ -413,6 +447,7 @@ func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Stat return buf.String() } +// Format goto statement func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { var buf bytes.Buffer @@ -422,6 +457,7 @@ func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { return buf.String() } +// Format goto destination statement func (f *Formatter) formatGotoDestinationStatement(stmt *ast.GotoDestinationStatement) string { var buf bytes.Buffer @@ -430,6 +466,7 @@ func (f *Formatter) formatGotoDestinationStatement(stmt *ast.GotoDestinationStat return buf.String() } +// Format function calling statement func (f *Formatter) formatFunctionCallStatement(stmt *ast.FunctionCallStatement) string { var buf bytes.Buffer From ed3d0a765d26b3b2cb6d6085131476de1e6a2995 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 3 Apr 2024 03:04:54 +0900 Subject: [PATCH 16/22] documentation --- config/config.go | 1 - config/config_test.go | 1 - docs/formatter.md | 174 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 5 deletions(-) diff --git a/config/config.go b/config/config.go index 72aec8a5..03d75c26 100644 --- a/config/config.go +++ b/config/config.go @@ -66,7 +66,6 @@ type FormatConfig struct { TrailingCommentWidth int `yaml:"trailing_comment_width" default:"2"` IndentStyle string `yaml:"indent_style" default:"space"` LineWidth int `yaml:"line_width" default:"120"` - BoolUpperCase bool `yaml:"bool_uppercase" default:"false"` ExplicitStringConat bool `yaml:"explicit_string_concat" default:"false"` SortDeclarationProperty bool `yaml:"sort_declaration_property" default:"false"` AlignDeclarationProperty bool `yaml:"align_declaration_property" default:"false"` diff --git a/config/config_test.go b/config/config_test.go index faf251f5..902ee1c8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -68,7 +68,6 @@ func TestConfigFromCLI(t *testing.T) { TrailingCommentWidth: 2, LineWidth: 120, IndentStyle: "space", - BoolUpperCase: false, ExplicitStringConat: false, SortDeclarationProperty: false, AlignDeclarationProperty: false, diff --git a/docs/formatter.md b/docs/formatter.md index 0206e244..655d1222 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -49,7 +49,7 @@ Supporting rules are described the following table and sections. | Name (configuration field) | Type | Default | Description | |:----------------------------|:------:|:-------:|------------------------------------------------------------------------------------------------| | indent_width | INT | 2 | Specify indent width | -| indent_style | STRING | space | Specify indent style character. `space(whitespace)` or `tab(\t)` is accepted | +| indent_style | STRING | space | Specify indent style character. Either `space(whitespace)` or `tab(\t)` is accepted | | trailing_comment_width | INT | 2 | Specify space size for trailing comment | | line_width | INT | 120 | Specify max characters for each line. The overflowed characters are displayed at the next line | | explicit_string_concat | BOOL | false | Explicitly write string concatenation operator `+` between expressions | @@ -57,6 +57,10 @@ Supporting rules are described the following table and sections. | align_declaration_property | BOOL | false | If true, align declaration properties like table, backend and director | | else_if | BOOL | false | Coerce use `else if` keyword for another if statement | | return_statement_parentheis | BOOL | true | Coerce surrounded return statement ident by parenthesis | +| sort_declaration | BOOL | false | Sort root declaration by specific order | +| align_trailing_comment | BOOL | false | Align trailing comments | +| comment_style | BOOL | false | Coerce comment character. Either `sharp(#)` or `slash(/)` is accepted | +| should_use_unset | BOOL | false | Replace `remove` statement into `unset` statemnt | ### Indent Width @@ -134,8 +138,11 @@ sub vcl_recv { Specify max characters for each line. The overflowed characters are printed at the next line with the same ident. -[!IMPORTANT] +``` Inserting line-feed is judged for each expression. It means formatter does not split in the middle of a sentence. +``` + +You can prevent this formatting rule by providing `-1` value to this configuration ```vcl sub vcl_recv { @@ -336,10 +343,171 @@ sub vcl_recv { } ``` -Formatted (recommend: true): +Formatted (return_statement_parenthesis: true): ```vcl sub vcl_recv { return (lookup); } ``` + +--- + +## Sort Declaration + +**default:false** + +If true, sort root declarations by VCL directive order: + +1. import/include statements +2. ACL +3. Backend +4. Director +5. Table +6. vcl_recv +7. vcl_hash +8. vcl_hit +9. vcl_miss +10. vcl_pass +11. vcl_fetch +12. vcl_error +13. vcl_deliver +14. vcl_log +15. user-defined subroutines + +```vcl +sub vcl_fetch { + ... +} + +sub vcl_deliver { + ... +} + +sub vcl_recv { + ... +} +``` + +Formatted (sort_declaration: true): + +```vcl +sub vcl_recv { + ... +} + +sub vcl_fetch { + ... +} + +sub vcl_deliver { + ... +} +``` + +--- + +## Align Trailing Comment + +**default:false** + +If true, align the trailing comment for each statement/declaration line: + +```vcl +sub vcl_recv { + set req.http.X-Forwarded-Host = "example.com"; // request comes from example.com + set req.backend = example_com; // set backend +} +``` + +Formatted (align_trailing_comment: true): + + +```vcl +sub vcl_recv { + set req.http.X-Forwarded-Host = "example.com"; // request comes from example.com + set req.backend = example_com; // set backend +} +``` + +--- + +## Always Next Line ElseIf + +**default:false** + +If true, the `else if` and `else` statement prints to the next line. + +```vcl +sub vcl_recv { + if (req.http.Foo) { + ... + } else if (req.http.Bar) { + ... + } else { + ... + } +} +``` + +Formatted (always_nest_line_else_if: true): + +```vcl +sub vcl_recv { + if (req.http.Foo) { + ... + } + else if (req.http.Bar) { + ... + } + else { + ... + } +} +``` + +--- + +## Comment Style + +**default:none** + +Define the comment style. `sharp` value will use `#` character, or `slash` value will use `/` character. +Note that the inline style comment `/* ... */` does not replace. + +```vcl +// Some leading comment +sub vcl_recv { +} +``` + +Formatted (comment_style: sharp): + +```vcl +## Some leading comment +sub vcl_recv { +} +``` + +--- + +## Should Use Unset + +**default:false** + +The `remove` statement is alias of `unset` statement, it can be replaced if this rule is true. + +```vcl +sub vcl_recv { + remove req.http.Foo; +} +``` + +Formatted (should_use_unset: true): + +```vcl +sub vcl_recv { + unset req.http.Foo; +} +``` + From 981697cb93ab61ee8e0f4d843192885fb9961fca Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Wed, 3 Apr 2024 03:05:52 +0900 Subject: [PATCH 17/22] tweak documentation --- docs/formatter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/formatter.md b/docs/formatter.md index 655d1222..985d006b 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -59,7 +59,7 @@ Supporting rules are described the following table and sections. | return_statement_parentheis | BOOL | true | Coerce surrounded return statement ident by parenthesis | | sort_declaration | BOOL | false | Sort root declaration by specific order | | align_trailing_comment | BOOL | false | Align trailing comments | -| comment_style | BOOL | false | Coerce comment character. Either `sharp(#)` or `slash(/)` is accepted | +| comment_style | STRING | none | Coerce comment character. Either `sharp(#)` or `slash(/)` is accepted | | should_use_unset | BOOL | false | Replace `remove` statement into `unset` statemnt | From c440bc1c67ef3b42ab28c740c0538d9b43ec72ff Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Thu, 4 Apr 2024 06:09:08 +0900 Subject: [PATCH 18/22] fix complecated expression including comments --- formatter/chunk_buffer.go | 238 ++++++++++++++++++++++++--- formatter/declaration_format.go | 9 + formatter/declaration_format_test.go | 99 +++++++++++ formatter/expression_format.go | 43 +---- formatter/formatter.go | 5 + formatter/statement_format.go | 5 +- go.mod | 1 - go.sum | 2 - parser/statement_parser.go | 2 +- 9 files changed, 345 insertions(+), 59 deletions(-) diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 29614f52..5c558a39 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -7,18 +7,63 @@ import ( "github.com/ysugimoto/falco/config" ) +// This map is used for the expression can be chunked or not. +// Following operators must be printed on a single line, otherwise VCL will cause syntax error. +var mustSingleOperators = map[string]struct{}{ + "==": {}, + "!=": {}, + "~": {}, + "!~": {}, + ">": {}, + "<": {}, + ">=": {}, + "<=": {}, +} + +// Chunk struct represents a piece of expression token +type Chunk struct { + buffer string + isComment bool + isOperator bool +} + +// isLineComment() returns true if chunk buffer is line comment that start with "#" or "//" +func (c *Chunk) isLineComment() bool { + if !c.isComment { + return false + } + + prefix := make([]byte, 2) + prefix[0] = c.buffer[0] + if len(c.buffer) > 1 { + prefix[1] = c.buffer[1] + } + return string(prefix) != "/*" +} + // ChunkBuffer struct reperesents limited-line chunked string from configration. type ChunkBuffer struct { - chunks []string + chunks []*Chunk conf *config.FormatConfig + index int } // Create ChunkBuffer pointer func newBuffer(c *config.FormatConfig) *ChunkBuffer { return &ChunkBuffer{ - chunks: []string{}, + chunks: []*Chunk{}, conf: c, + index: -1, + } +} + +// Get next chunk +func (c *ChunkBuffer) nextChunk() *Chunk { + if c.index+1 > len(c.chunks)-1 { + return nil } + c.index++ + return c.chunks[c.index] } // Merge buffers @@ -26,14 +71,60 @@ func (c *ChunkBuffer) Merge(nc *ChunkBuffer) { c.chunks = append(c.chunks, nc.chunks...) } +// Write operator string to buffer +func (c *ChunkBuffer) WriteOperator(op string) { + c.chunks = append(c.chunks, &Chunk{ + buffer: op, + isOperator: true, + }) +} + +// Write "comment" string to buffer +func (c *ChunkBuffer) WriteComment(comment string) { + c.chunks = append(c.chunks, &Chunk{ + buffer: comment, + isComment: true, + }) +} + // Write string to buffer - same as bytes.Buffer func (c *ChunkBuffer) WriteString(s string) { - c.chunks = append(c.chunks, s) + c.chunks = append(c.chunks, &Chunk{ + buffer: s, + }) } // Get "No" chunked string func (c *ChunkBuffer) String() string { - return strings.Join(c.chunks, "") + var buf bytes.Buffer + + for i := range c.chunks { + buf.WriteString(c.chunks[i].buffer) + if c.chunks[i].isLineComment() { + buf.WriteString("\n") + } + } + + return buf.String() +} + +// ChunkState represents generating chunk strings +type ChunkState struct { + lineWidth int + level int + offset int + head int + count int +} + +// isHead() returns true is current state is the head of line +func (s *ChunkState) isHead() bool { + return s.count == s.head +} + +// Reset state +func (s *ChunkState) reset() { + s.count = s.head } // Calculate line-chunked strings @@ -43,27 +134,136 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { return c.String() } + state := &ChunkState{ + lineWidth: c.conf.LineWidth, + level: level, + offset: offset, + head: offset + level*c.conf.IndentWidth, + count: offset + level*c.conf.IndentWidth, + } + var buf bytes.Buffer - count := offset + level*c.conf.IndentWidth - for i, b := range c.chunks { - // If adding next expression overflows line-width limit, insert line-feed and adjust indent - if count+len(b) > c.conf.LineWidth { - buf.WriteString("\n") - buf.WriteString(indent(c.conf, level)) - if offset > 0 { - buf.WriteString(c.offsetString(offset)) + for { + chunk := c.nextChunk() + if chunk == nil { + return strings.TrimSpace(buf.String()) + } + + switch { + // "//", "#" comment, need to print next line + case chunk.isLineComment(): + buf.WriteString(c.chunkLineComment(state, chunk)) + // infix or group operator + case chunk.isOperator: + // If group operator, inside expressions should be printed on the same line + if chunk.buffer == "(" { + if next := c.nextChunk(); next != nil { + buf.WriteString(c.chunkGroupOperator(state, next)) + } + continue + } + // Or, the operator is the member os mustSingleOperators, the expression must be printed on the same line + if _, ok := mustSingleOperators[chunk.buffer]; ok { + buf.WriteString(c.chunkInfixOperator(state, chunk)) + continue } - count = offset + level*c.conf.IndentWidth - } else if i != 0 { - buf.WriteString(" ") - count++ + buf.WriteString(c.chunkString(state, chunk.buffer)) + // Otherwise (token, inline comment), create chunk string + default: + buf.WriteString(c.chunkString(state, chunk.buffer)) + } + } +} + +// nextLine() returns line feed and indent string +func (c *ChunkBuffer) nextLine(state *ChunkState) string { + out := "\n" + indent(c.conf, state.level) + if state.offset > 0 { + out += c.offsetString(state.offset) + } + return out +} + +// chunkLineComment() returns chunk string of line comment +func (c *ChunkBuffer) chunkLineComment(state *ChunkState, chunk *Chunk) string { + var buf bytes.Buffer + + if !state.isHead() { + buf.WriteString(c.nextLine(state)) + } + buf.WriteString(chunk.buffer) + buf.WriteString(c.nextLine(state)) + state.reset() + + return buf.String() +} + +// chunkGroupOperator() returns chunk group expression string +func (c *ChunkBuffer) chunkGroupOperator(state *ChunkState, chunk *Chunk) string { + expr := chunk.buffer + + for { + next := c.nextChunk() + if next == nil { + return c.chunkString(state, "("+expr+")") } - buf.WriteString(b) - count += len(b) + + switch { + case next.isLineComment(): + expr += next.buffer + expr += c.nextLine(state) + state.reset() + case next.buffer == ")": + return c.chunkString(state, "("+expr+")") + default: + expr += " " + next.buffer + } + } +} + +// chunkInfixOperator() returns chunk infix expression string +func (c *ChunkBuffer) chunkInfixOperator(state *ChunkState, chunk *Chunk) string { + expr := chunk.buffer + + for { + next := c.nextChunk() + if next == nil { + return c.chunkString(state, expr) + } + + switch { + case next.isLineComment(): + expr += next.buffer + expr += c.nextLine(state) + state.reset() + case next.isComment: + expr += " " + next.buffer + default: + expr += " " + next.buffer + return c.chunkString(state, expr) + } + } +} + +// chunkString() returns chunked string +func (c *ChunkBuffer) chunkString(state *ChunkState, expr string) string { + var buf bytes.Buffer + var prefix string + + if !state.isHead() { + prefix = " " + } + + if state.count+len(prefix+expr) > state.lineWidth { + buf.WriteString(c.nextLine(state)) + state.reset() + prefix = "" } + buf.WriteString(prefix + expr) + state.count += len(prefix + expr) - return strings.TrimSpace(buf.String()) + return buf.String() } // Padding offset string diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index 4a5a59ba..59585a7c 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "sort" + "strings" "github.com/ysugimoto/falco/ast" ) @@ -324,6 +325,14 @@ func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) var buf bytes.Buffer buf.WriteString("sub " + decl.Name.Value + " ") + // Functional Subroutine + if decl.ReturnType != nil { + buf.WriteString(strings.ToUpper(decl.ReturnType.Value) + " ") + f.isFunctionalSubroutine = true // flag turns on + defer func() { + f.isFunctionalSubroutine = false + }() + } buf.WriteString(f.formatBlockStatement(decl.Block)) return &Declaration{ diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 9e9b3065..cf806e6b 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -534,3 +534,102 @@ sub vcl_recv { }) } } + +func TestFunctionalSubroutne(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "basic formartting", + input: `sub boolfn BOOL { + return true; +}`, + expect: `sub boolfn BOOL { + return true; +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} + +func TestComplicatedExpressions(t *testing.T) { + tests := []struct { + name string + input string + expect string + conf *config.FormatConfig + }{ + { + name: "expression includes line comment", + input: `sub vcl_recv { + if ( + // foo + req.http.foo == "bar" + // bar + && req.http.bar == "baz" + ) { + esi; + } elseif (req.http.foo) { + esi; + } +}`, + expect: `sub vcl_recv { + if ( + // foo + req.http.foo == "bar" + // bar + && req.http.bar == "baz" + ) { + esi; + } elseif (req.http.foo) { + esi; + } +} +`, + }, + { + name: "grouped expression", + input: `sub vcl_recv { + if ( + (req.http.foo == "bar" && req.http.bar == "baz") || (req.http.A == "B" && req.http.C == "D") + ) { + esi; + } elseif (req.http.foo) { + esi; + } +}`, + expect: `sub vcl_recv { + if ( + (req.http.foo == "bar" && req.http.bar == "baz") || + (req.http.A == "B" && req.http.C == "D") + ) { + esi; + } elseif (req.http.foo) { + esi; + } +} +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 70, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert(t, tt.input, tt.expect, tt.conf) + }) + } +} diff --git a/formatter/expression_format.go b/formatter/expression_format.go index 06af6805..0caed1d0 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -14,7 +14,7 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { // leading comment if v := f.formatComment(expr.GetMeta().Leading, "", 0); v != "" { - buf.WriteString(v + " ") + buf.WriteComment(v) } switch t := expr.(type) { @@ -49,7 +49,7 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { // trailing comment if v := f.formatComment(expr.GetMeta().Trailing, "", 0); v != "" { - buf.WriteString(" " + v) + buf.WriteComment(v) } return buf @@ -102,19 +102,6 @@ func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuf return buf } -// This map is used for the expression can be chunked or not. -// Following operators must be printed on a single line, otherwise VCL will cause syntax error. -var mustSingleOperators = map[string]struct{}{ - "==": {}, - "!=": {}, - "~": {}, - "!~": {}, - ">": {}, - "<": {}, - ">=": {}, - "<=": {}, -} - func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffer { buf := f.chunkBuffer() @@ -125,24 +112,12 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe } } - // If operator must be printed on a single line, add combinated string to ChunkBuffer - if _, ok := mustSingleOperators[operator]; ok { - buf.WriteString( - fmt.Sprintf( - "%s %s %s", - f.formatExpression(expr.Left).String(), - operator, - f.formatExpression(expr.Right).String(), - ), - ) - } else { - // Otherwise, the exprssion can be printed to newline - buf.Merge(f.formatExpression(expr.Left)) - if operator != "" { - buf.WriteString(operator) - } - buf.Merge(f.formatExpression(expr.Right)) + buf.Merge(f.formatExpression(expr.Left)) + if operator != "" { + buf.WriteOperator(operator) } + buf.Merge(f.formatExpression(expr.Right)) + return buf } @@ -165,9 +140,9 @@ func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkBuffer { buf := f.chunkBuffer() - buf.WriteString("(") + buf.WriteOperator("(") buf.Merge(f.formatExpression(expr.Right)) - buf.WriteString(")") + buf.WriteOperator(")") return buf } diff --git a/formatter/formatter.go b/formatter/formatter.go index c822fca1..f4e350bf 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -14,6 +14,11 @@ import ( type Formatter struct { // Defined at config package, all built-in formatting rules includes with default values conf *config.FormatConfig + + // statful formatting - flag for formatting inside functional subroutine. + // If this flag turns on, the return statement MUST return the subroutine return type, not a state + // so it should not wrap the parenthesis even if configuration is true. + isFunctionalSubroutine bool } // Create Formatter pointer diff --git a/formatter/statement_format.go b/formatter/statement_format.go index f3e4cab0..4719b507 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -411,8 +411,9 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { if stmt.ReturnExpression != nil { prefix := " " suffix := "" - // If ReturnStatementParenthesis is enabled, must be surrounded by parenthesis - if f.conf.ReturnStatementParenthesis { + // If ReturnStatementParenthesis is enabled and inside functional subroutine, + // the return argument must be surrounded by parenthesis + if f.conf.ReturnStatementParenthesis && !f.isFunctionalSubroutine { prefix = " (" suffix = ")" } diff --git a/go.mod b/go.mod index ed7108fd..c6b0ef79 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect diff --git a/go.sum b/go.sum index 27ee6803..924b3d51 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= -github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/parser/statement_parser.go b/parser/statement_parser.go index 1b5249ed..6260a688 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -580,7 +580,7 @@ func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { // Note: VCL could define "else if" statement with "elseif", "elsif" keyword case token.ELSEIF, token.ELSIF: // elseif, elsif p.nextToken() // point to ELSEIF/ELSIF - another, err := p.parseAnotherIfStatement(p.peekToken.Token.Literal) + another, err := p.parseAnotherIfStatement(p.curToken.Token.Literal) if err != nil { return nil, errors.WithStack(err) } From a2f4bae90f4fd42de27941f93803f9a3da2b33ba Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Thu, 4 Apr 2024 06:48:11 +0900 Subject: [PATCH 19/22] refactor a little --- formatter/chunk_buffer.go | 82 +++++++++++++++++----------------- formatter/chunk_buffer_test.go | 4 +- formatter/expression_format.go | 44 +++++++++--------- 3 files changed, 64 insertions(+), 66 deletions(-) diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 5c558a39..492454a5 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -20,16 +20,24 @@ var mustSingleOperators = map[string]struct{}{ "<=": {}, } +type ChunkType int + +const ( + Token ChunkType = iota + 1 + Comment + Operator + Group +) + // Chunk struct represents a piece of expression token type Chunk struct { - buffer string - isComment bool - isOperator bool + buffer string + Type ChunkType } // isLineComment() returns true if chunk buffer is line comment that start with "#" or "//" func (c *Chunk) isLineComment() bool { - if !c.isComment { + if c.Type != Comment { return false } @@ -66,31 +74,16 @@ func (c *ChunkBuffer) nextChunk() *Chunk { return c.chunks[c.index] } -// Merge buffers -func (c *ChunkBuffer) Merge(nc *ChunkBuffer) { +// Append buffers +func (c *ChunkBuffer) Append(nc *ChunkBuffer) { c.chunks = append(c.chunks, nc.chunks...) } -// Write operator string to buffer -func (c *ChunkBuffer) WriteOperator(op string) { - c.chunks = append(c.chunks, &Chunk{ - buffer: op, - isOperator: true, - }) -} - -// Write "comment" string to buffer -func (c *ChunkBuffer) WriteComment(comment string) { - c.chunks = append(c.chunks, &Chunk{ - buffer: comment, - isComment: true, - }) -} - -// Write string to buffer - same as bytes.Buffer -func (c *ChunkBuffer) WriteString(s string) { +// Write buffer to buffer with ChunkType +func (c *ChunkBuffer) Write(s string, t ChunkType) { c.chunks = append(c.chunks, &Chunk{ buffer: s, + Type: t, }) } @@ -150,26 +143,29 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { return strings.TrimSpace(buf.String()) } - switch { - // "//", "#" comment, need to print next line - case chunk.isLineComment(): - buf.WriteString(c.chunkLineComment(state, chunk)) - // infix or group operator - case chunk.isOperator: - // If group operator, inside expressions should be printed on the same line - if chunk.buffer == "(" { - if next := c.nextChunk(); next != nil { - buf.WriteString(c.chunkGroupOperator(state, next)) - } + switch chunk.Type { + case Comment: + // "//", "#" comment, need to print next line + if chunk.isLineComment() { + buf.WriteString(c.chunkLineComment(state, chunk)) continue } + buf.WriteString(c.chunkString(state, chunk.buffer)) + // group operator + case Group: + // If group operator, inside expressions should be printed on the same line + if next := c.nextChunk(); next != nil { + buf.WriteString(c.chunkGroupOperator(state, next)) + } + // infix operator + case Operator: // Or, the operator is the member os mustSingleOperators, the expression must be printed on the same line if _, ok := mustSingleOperators[chunk.buffer]; ok { buf.WriteString(c.chunkInfixOperator(state, chunk)) continue } buf.WriteString(c.chunkString(state, chunk.buffer)) - // Otherwise (token, inline comment), create chunk string + // Otherwise (token), create chunk string default: buf.WriteString(c.chunkString(state, chunk.buffer)) } @@ -232,12 +228,14 @@ func (c *ChunkBuffer) chunkInfixOperator(state *ChunkState, chunk *Chunk) string return c.chunkString(state, expr) } - switch { - case next.isLineComment(): - expr += next.buffer - expr += c.nextLine(state) - state.reset() - case next.isComment: + switch next.Type { + case Comment: + if next.isLineComment() { + expr += next.buffer + expr += c.nextLine(state) + state.reset() + continue + } expr += " " + next.buffer default: expr += " " + next.buffer diff --git a/formatter/chunk_buffer_test.go b/formatter/chunk_buffer_test.go index f9c844c0..b693b053 100644 --- a/formatter/chunk_buffer_test.go +++ b/formatter/chunk_buffer_test.go @@ -66,7 +66,7 @@ incididunt ut labore et dolore magna aliqua`, IndentWidth: 2, }) for _, c := range tt.input { - cb.WriteString(c) + cb.Write(c, Token) } chunk := cb.ChunkedString(0, 0) if diff := cmp.Diff(tt.expect, chunk); diff != "" { @@ -109,7 +109,7 @@ func TestChunkBufferInExpression(t *testing.T) { IndentWidth: 2, }) for _, c := range tt.input { - cb.WriteString(c) + cb.Write(c, Token) } chunk := cb.ChunkedString(0, buf.Len()) buf.WriteString(chunk) diff --git a/formatter/expression_format.go b/formatter/expression_format.go index 0caed1d0..49735726 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -14,42 +14,42 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { // leading comment if v := f.formatComment(expr.GetMeta().Leading, "", 0); v != "" { - buf.WriteComment(v) + buf.Write(v, Comment) } switch t := expr.(type) { // Primitive types return string case *ast.Ident: - buf.WriteString(f.formatIdent(t)) + buf.Write(f.formatIdent(t), Token) case *ast.IP: - buf.WriteString(f.formatIP(t)) + buf.Write(f.formatIP(t), Token) case *ast.Boolean: - buf.WriteString(f.formatBoolean(t)) + buf.Write(f.formatBoolean(t), Token) case *ast.Integer: - buf.WriteString(f.formatInteger(t)) + buf.Write(f.formatInteger(t), Token) case *ast.String: - buf.WriteString(f.formatString(t)) + buf.Write(f.formatString(t), Token) case *ast.Float: - buf.WriteString(f.formatFloat(t)) + buf.Write(f.formatFloat(t), Token) case *ast.RTime: - buf.WriteString(f.formatRTime(t)) + buf.Write(f.formatRTime(t), Token) case *ast.FunctionCallExpression: - buf.WriteString(f.formatFunctionCallExpression(t)) + buf.Write(f.formatFunctionCallExpression(t), Token) case *ast.IfExpression: - buf.WriteString(f.formatIfExpression(t)) + buf.Write(f.formatIfExpression(t), Token) // Combinated expressions return *ChunkBuffer to merge case *ast.PrefixExpression: - buf.Merge(f.formatPrefixExpression(t)) + buf.Append(f.formatPrefixExpression(t)) case *ast.GroupedExpression: - buf.Merge(f.formatGroupedExpression(t)) + buf.Append(f.formatGroupedExpression(t)) case *ast.InfixExpression: - buf.Merge(f.formatInfixExpression(t)) + buf.Append(f.formatInfixExpression(t)) } // trailing comment if v := f.formatComment(expr.GetMeta().Trailing, "", 0); v != "" { - buf.WriteComment(v) + buf.Write(v, Comment) } return buf @@ -96,8 +96,8 @@ func (f *Formatter) formatRTime(expr *ast.RTime) string { func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuffer { buf := f.chunkBuffer() - buf.WriteString(expr.Operator) - buf.Merge(f.formatExpression(expr.Right)) + buf.Write(expr.Operator, Operator) + buf.Append(f.formatExpression(expr.Right)) return buf } @@ -112,11 +112,11 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe } } - buf.Merge(f.formatExpression(expr.Left)) + buf.Append(f.formatExpression(expr.Left)) if operator != "" { - buf.WriteOperator(operator) + buf.Write(operator, Operator) } - buf.Merge(f.formatExpression(expr.Right)) + buf.Append(f.formatExpression(expr.Right)) return buf } @@ -140,9 +140,9 @@ func (f *Formatter) formatIfExpression(expr *ast.IfExpression) string { func (f *Formatter) formatGroupedExpression(expr *ast.GroupedExpression) *ChunkBuffer { buf := f.chunkBuffer() - buf.WriteOperator("(") - buf.Merge(f.formatExpression(expr.Right)) - buf.WriteOperator(")") + buf.Write("(", Group) + buf.Append(f.formatExpression(expr.Right)) + buf.Write(")", Group) return buf } From bb2deee1a4b6d618adb4ecdce103400bc6149d22 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Thu, 4 Apr 2024 17:26:45 +0900 Subject: [PATCH 20/22] fix prefix expression formatting and add indent_case_labels rule --- config/config.go | 1 + docs/formatter.md | 46 ++++++++++++++++++++++++++++ formatter/chunk_buffer.go | 10 ++++-- formatter/declaration_format_test.go | 24 +++++++++++++++ formatter/expression_format.go | 4 +-- formatter/statement_format.go | 18 ++++++++--- formatter/statement_format_test.go | 46 ++++++++++++++++++++++++++++ 7 files changed, 141 insertions(+), 8 deletions(-) diff --git a/config/config.go b/config/config.go index 03d75c26..5ed9daeb 100644 --- a/config/config.go +++ b/config/config.go @@ -76,6 +76,7 @@ type FormatConfig struct { AlignTrailingComment bool `yaml:"align_trailing_comment" default:"false"` CommentStyle string `yaml:"comment_style" default:"none"` ShouldUseUnset bool `yaml:"should_use_unset" default:"false"` + IndentCaseLabels bool `yaml:"indent_case_labels" default:"false"` } type Config struct { diff --git a/docs/formatter.md b/docs/formatter.md index 985d006b..f48e8a9c 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -61,6 +61,7 @@ Supporting rules are described the following table and sections. | align_trailing_comment | BOOL | false | Align trailing comments | | comment_style | STRING | none | Coerce comment character. Either `sharp(#)` or `slash(/)` is accepted | | should_use_unset | BOOL | false | Replace `remove` statement into `unset` statemnt | +| indent_case_labels | BOOL | false | If true, add indent for each `case` statement in `switch` | ### Indent Width @@ -511,3 +512,48 @@ sub vcl_recv { } ``` +--- + +## Indent Case Labels + +By default format the placement of case labels at the same indentation level as the outer block. +This keeps code in the case blocks indented the same amount as they would be in an if-else-if chain. + +**default: false** + +```vcl +switch (req.http.foo) { + case "bar": + break; + case "baz": + break; +default: + break; +} +``` + +Formatted (false): + +```vcl +switch (req.http.foo) { +case "bar": + break; +case "baz": + break; +default: + break; +} +``` + +Formatted (true): + +```vcl +switch (req.http.foo) { + case "bar": + break; + case "baz": + break; + default: + break; +} +``` diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 492454a5..6ee4290c 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -25,8 +25,9 @@ type ChunkType int const ( Token ChunkType = iota + 1 Comment - Operator + Infix Group + Prefix ) // Chunk struct represents a piece of expression token @@ -151,6 +152,11 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { continue } buf.WriteString(c.chunkString(state, chunk.buffer)) + // prefix operator + case Prefix: + if next := c.nextChunk(); next != nil { + buf.WriteString(chunk.buffer + next.buffer) + } // group operator case Group: // If group operator, inside expressions should be printed on the same line @@ -158,7 +164,7 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { buf.WriteString(c.chunkGroupOperator(state, next)) } // infix operator - case Operator: + case Infix: // Or, the operator is the member os mustSingleOperators, the expression must be printed on the same line if _, ok := mustSingleOperators[chunk.buffer]; ok { buf.WriteString(c.chunkInfixOperator(state, chunk)) diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index cf806e6b..405f20c6 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -617,6 +617,30 @@ func TestComplicatedExpressions(t *testing.T) { esi; } } +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 70, + }, + }, + { + name: "prefix expression", + input: `sub vcl_recv { + if (! req.http.Foo) { + esi; + } elseif (req.http.foo) { + esi; + } +}`, + expect: `sub vcl_recv { + if (!req.http.Foo) { + esi; + } elseif (req.http.foo) { + esi; + } +} `, conf: &config.FormatConfig{ IndentWidth: 2, diff --git a/formatter/expression_format.go b/formatter/expression_format.go index 49735726..04271516 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -96,7 +96,7 @@ func (f *Formatter) formatRTime(expr *ast.RTime) string { func (f *Formatter) formatPrefixExpression(expr *ast.PrefixExpression) *ChunkBuffer { buf := f.chunkBuffer() - buf.Write(expr.Operator, Operator) + buf.Write(expr.Operator, Prefix) buf.Append(f.formatExpression(expr.Right)) return buf @@ -114,7 +114,7 @@ func (f *Formatter) formatInfixExpression(expr *ast.InfixExpression) *ChunkBuffe buf.Append(f.formatExpression(expr.Left)) if operator != "" { - buf.Write(operator, Operator) + buf.Write(operator, Infix) } buf.Append(f.formatExpression(expr.Right)) diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 4719b507..68c2c14c 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -274,8 +274,13 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { buf.WriteString("switch (" + f.formatExpression(stmt.Control).String() + ") {\n") for _, c := range stmt.Cases { + // If indent_cale_labels is false, subtrat 1 nest level + if !f.conf.IndentCaseLabels { + c.Meta.Nest-- + } buf.WriteString(f.formatComment(c.Leading, "\n", c.Meta.Nest)) buf.WriteString(f.indent(c.Meta.Nest)) + if c.Test != nil { buf.WriteString("case ") if c.Test.Operator == "~" { @@ -303,17 +308,22 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { lines := Lines{} for _, stmt := range cs.Statements { - if stmt.GetMeta().PreviousEmptyLines > 0 { + meta := stmt.GetMeta() + // If indent_cale_labels is false, subtrat 1 nest level + if !f.conf.IndentCaseLabels { + meta.Nest-- + } + if meta.PreviousEmptyLines > 0 { group.Lines = append(group.Lines, lines) lines = Lines{} } // need to plus 1 to nested indent because parser won't increase nest level line := &Line{ - Leading: f.formatComment(stmt.GetMeta().Leading, "\n", stmt.GetMeta().Nest+1), - Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", stmt.GetMeta().Nest+1), + Leading: f.formatComment(stmt.GetMeta().Leading, "\n", meta.Nest+1), + Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", meta.Nest+1), } if _, ok := stmt.(*ast.BreakStatement); ok { - line.Buffer = f.indent(stmt.GetMeta().Nest+1) + "break;" + line.Buffer = f.indent(meta.Nest+1) + "break;" } else { line.Buffer = f.formatStatement(stmt).String() } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 52181db5..15ccabb5 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -825,6 +825,45 @@ func TestFormatSwitchStatement(t *testing.T) { // switch infix comment } // trailing comment } +`, + expect: `sub vcl_recv { + // switch leading comment + switch (req.http.Foo) { + // first case comment + case "bar": + // break leading comment + break; + // second case comment + case "baz": + // break leading comment + break; + // default case comment + default: + break; + // switch infix comment + } // trailing comment +} +`, + }, + { + name: "indent case labels", + input: `sub vcl_recv { + // switch leading comment + switch(req.http.Foo) { + // first case comment + case "bar": + // break leading comment + break; + // second case comment + case "baz": + // break leading comment + break; + // default case comment + default: + break; + // switch infix comment + } // trailing comment +} `, expect: `sub vcl_recv { // switch leading comment @@ -844,6 +883,13 @@ func TestFormatSwitchStatement(t *testing.T) { } // trailing comment } `, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 70, + IndentCaseLabels: true, + }, }, } From 7ba2ae0c29f5967ad1d6cb9679344cd6b593cc5a Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Tue, 30 Apr 2024 03:17:54 +0900 Subject: [PATCH 21/22] wip merge and refactoring --- ast/return_statement.go | 6 +- ast/switch_statement.go | 3 + ast/switch_statement_test.go | 3 +- formatter/chunk_buffer.go | 53 ++++-- formatter/declaration_format.go | 41 +++-- formatter/declaration_format_test.go | 243 ++++++++++----------------- formatter/expression_format.go | 10 +- formatter/helper.go | 26 +++ formatter/statement_format.go | 173 +++++++++++++------ formatter/statement_format_test.go | 235 +++++++++++--------------- go.mod | 1 + go.sum | 2 + parser/declaration_parser.go | 8 + parser/parser_test.go | 4 +- parser/statement_parser.go | 10 +- 15 files changed, 442 insertions(+), 376 deletions(-) diff --git a/ast/return_statement.go b/ast/return_statement.go index 19bab6cf..ee7d274f 100644 --- a/ast/return_statement.go +++ b/ast/return_statement.go @@ -33,7 +33,11 @@ func (r *ReturnStatement) String() string { buf.WriteString(indent(r.Nest) + "return " + r.ReturnExpression.String() + ";") } } else { - buf.WriteString(indent(r.Nest) + "return;") + buf.WriteString(indent(r.Nest) + "return") + if v := r.InfixComment(inline); v != "" { + buf.WriteString(paddingLeft(v)) + } + buf.WriteString(";") } buf.WriteString(r.TrailingComment(inline)) buf.WriteString("\n") diff --git a/ast/switch_statement.go b/ast/switch_statement.go index b36a83fd..9911d01a 100644 --- a/ast/switch_statement.go +++ b/ast/switch_statement.go @@ -22,6 +22,9 @@ func (s *SwitchStatement) String() string { buf.WriteString(stmt.String()) } + if v := s.InfixComment(lineFeed); v != "" { + buf.WriteString(" " + v) + } buf.WriteString("}") buf.WriteString(s.TrailingComment(inline)) buf.WriteString("\n") diff --git a/ast/switch_statement_test.go b/ast/switch_statement_test.go index 8c894528..097b7f87 100644 --- a/ast/switch_statement_test.go +++ b/ast/switch_statement_test.go @@ -6,7 +6,7 @@ import ( func TestSwitchStatement(t *testing.T) { switchs := &SwitchStatement{ - Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")), + Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */"), comments("// This is switch infix")), Control: &SwitchControl{ Meta: New(T, 0, comments("/* before_paren */"), comments("/* after_paren */")), Expression: &Ident{ @@ -112,6 +112,7 @@ case "5": /* This is comment */ default: /* This is comment */ // This is comment break; /* This is comment */ + // This is switch infix } /* This is comment */ ` diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 6ee4290c..7bb9f01b 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -75,6 +75,14 @@ func (c *ChunkBuffer) nextChunk() *Chunk { return c.chunks[c.index] } +// Get peek chunk +func (c *ChunkBuffer) peekChunk() *Chunk { + if c.index+1 > len(c.chunks)-1 { + return nil + } + return c.chunks[c.index+1] +} + // Append buffers func (c *ChunkBuffer) Append(nc *ChunkBuffer) { c.chunks = append(c.chunks, nc.chunks...) @@ -96,6 +104,8 @@ func (c *ChunkBuffer) String() string { buf.WriteString(c.chunks[i].buffer) if c.chunks[i].isLineComment() { buf.WriteString("\n") + } else if i < len(c.chunks)-1 { + buf.WriteString(" ") } } @@ -165,19 +175,38 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { } // infix operator case Infix: - // Or, the operator is the member os mustSingleOperators, the expression must be printed on the same line - if _, ok := mustSingleOperators[chunk.buffer]; ok { - buf.WriteString(c.chunkInfixOperator(state, chunk)) - continue - } + // Or, the operator is the member of mustSingleOperators, the expression must be printed on the same line + // if _, ok := mustSingleOperators[chunk.buffer]; ok { + // buf.WriteString(c.chunkInfixOperator(state, chunk)) + // continue + // } buf.WriteString(c.chunkString(state, chunk.buffer)) // Otherwise (token), create chunk string default: + chunk.buffer += c.combineInfixChunk(chunk.buffer) buf.WriteString(c.chunkString(state, chunk.buffer)) } } } +func (c *ChunkBuffer) combineInfixChunk(b string) string { + peek := c.peekChunk() + if peek == nil || peek.Type != Infix { + return "" + } + // Infix operator + _, ok := mustSingleOperators[peek.buffer] + if !ok { + return "" + } + expr := " " + c.nextChunk().buffer + if next := c.nextChunk(); next != nil { + expr += " " + next.buffer + } + + return expr +} + // nextLine() returns line feed and indent string func (c *ChunkBuffer) nextLine(state *ChunkState) string { out := "\n" + indent(c.conf, state.level) @@ -191,10 +220,10 @@ func (c *ChunkBuffer) nextLine(state *ChunkState) string { func (c *ChunkBuffer) chunkLineComment(state *ChunkState, chunk *Chunk) string { var buf bytes.Buffer - if !state.isHead() { - buf.WriteString(c.nextLine(state)) - } - buf.WriteString(chunk.buffer) + // if !state.isHead() { + // buf.WriteString(c.nextLine(state)) + // } + buf.WriteString(" " + chunk.buffer) buf.WriteString(c.nextLine(state)) state.reset() @@ -231,7 +260,8 @@ func (c *ChunkBuffer) chunkInfixOperator(state *ChunkState, chunk *Chunk) string for { next := c.nextChunk() if next == nil { - return c.chunkString(state, expr) + state.count += len(expr) + return expr } switch next.Type { @@ -245,7 +275,8 @@ func (c *ChunkBuffer) chunkInfixOperator(state *ChunkState, chunk *Chunk) string expr += " " + next.buffer default: expr += " " + next.buffer - return c.chunkString(state, expr) + state.count += len(expr) + return expr } } } diff --git a/formatter/declaration_format.go b/formatter/declaration_format.go index 59585a7c..31f4911e 100644 --- a/formatter/declaration_format.go +++ b/formatter/declaration_format.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "sort" - "strings" "github.com/ysugimoto/falco/ast" ) @@ -24,10 +23,16 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration if cidr.Inverse != nil && cidr.Inverse.Value { buf.WriteString("!") } - buf.WriteString(`"` + cidr.IP.String() + `"`) + if v := f.formatComment(cidr.IP.Leading, " ", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(`"` + cidr.IP.Value + `"`) if cidr.Mask != nil { buf.WriteString("/" + cidr.Mask.String()) } + if v := f.formatComment(cidr.IP.Trailing, " ", 0); v != "" { + buf.WriteString(" " + v) + } lines = append(lines, &DelclarationPropertyLine{ Leading: f.formatComment(cidr.Leading, "\n", 1), Trailing: f.trailing(cidr.Trailing), @@ -46,7 +51,7 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration } var buf bytes.Buffer - buf.WriteString("acl " + decl.Name.Value + " {\n") + buf.WriteString("acl " + decl.Name.String() + " {\n") buf.WriteString(group.String()) if len(decl.Infix) > 0 { buf.WriteString(f.indent(1)) @@ -65,7 +70,7 @@ func (f *Formatter) formatAclDeclaration(decl *ast.AclDeclaration) *Declaration func (f *Formatter) formatBackendDeclaration(decl *ast.BackendDeclaration) *Declaration { var buf bytes.Buffer - buf.WriteString("backend " + decl.Name.Value + " {\n") + buf.WriteString("backend " + decl.Name.String() + " {\n") buf.WriteString(f.formatBackendProperties(decl.Properties, 1)) if len(decl.Infix) > 0 { buf.WriteString(f.indent(1)) @@ -165,13 +170,19 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *De }) } for _, v := range t.Values { - line.Key += fmt.Sprintf(".%s = %s; ", v.Key.Value, f.formatExpression(v.Value)) + if v := f.formatComment(v.Leading, " ", 0); v != "" { + line.Key += v + } + line.Key += fmt.Sprintf(".%s = %s; ", v.Key.String(), v.Value.String()) + } + if len(t.Infix) > 0 { + line.Key += f.formatComment(t.Infix, " ", 0) } line.Key += "}" // Backend property is object, semicolon is not needed line.isObject = true case *ast.DirectorProperty: - line.Key += "." + t.Key.Value + line.Key += "." + t.Key.String() line.Operator = " = " line.Value = t.Value.String() line.EndCharacter = ";" @@ -195,7 +206,7 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *De } var buf bytes.Buffer - buf.WriteString("director " + decl.Name.Value + " " + decl.DirectorType.Value + " {\n") + buf.WriteString("director " + decl.Name.String() + " " + decl.DirectorType.String() + " {\n") buf.WriteString(group.String()) if len(decl.Infix) > 0 { buf.WriteString(f.indent(1)) @@ -214,9 +225,9 @@ func (f *Formatter) formatDirectorDeclaration(decl *ast.DirectorDeclaration) *De func (f *Formatter) formatTableDeclaration(decl *ast.TableDeclaration) *Declaration { var buf bytes.Buffer - buf.WriteString("table " + decl.Name.Value) + buf.WriteString("table " + decl.Name.String()) if decl.ValueType != nil { - buf.WriteString(" " + decl.ValueType.Value) + buf.WriteString(" " + decl.ValueType.String()) } buf.WriteString(" {\n") buf.WriteString(f.formatTableProperties(decl.Properties)) @@ -253,8 +264,8 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { Leading: f.formatComment(prop.Meta.Leading, "\n", 1), Trailing: f.trailing(prop.Meta.Trailing), Operator: ": ", - Key: f.indent(1) + f.formatString(prop.Key), - Value: f.formatExpression(prop.Value).String(), + Key: f.indent(1) + prop.Key.String(), + Value: prop.Value.String(), EndCharacter: ",", } lines = append(lines, line) @@ -282,7 +293,7 @@ func (f *Formatter) formatTableProperties(props []*ast.TableProperty) string { func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) *Declaration { var buf bytes.Buffer - buf.WriteString("penaltybox " + decl.Name.Value) + buf.WriteString("penaltybox " + decl.Name.String()) buf.WriteString(" {") // penaltybox does not have properties if len(decl.Block.Infix) > 0 { @@ -303,7 +314,7 @@ func (f *Formatter) formatPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaration) *Declaration { var buf bytes.Buffer - buf.WriteString("ratecounter " + decl.Name.Value) + buf.WriteString("ratecounter " + decl.Name.String()) buf.WriteString(" {") // ratecounter does not have properties if len(decl.Block.Infix) > 0 { @@ -324,10 +335,10 @@ func (f *Formatter) formatRatecounterDeclaration(decl *ast.RatecounterDeclaratio func (f *Formatter) formatSubroutineDeclaration(decl *ast.SubroutineDeclaration) *Declaration { var buf bytes.Buffer - buf.WriteString("sub " + decl.Name.Value + " ") + buf.WriteString("sub " + decl.Name.String() + " ") // Functional Subroutine if decl.ReturnType != nil { - buf.WriteString(strings.ToUpper(decl.ReturnType.Value) + " ") + buf.WriteString(decl.ReturnType.String() + " ") f.isFunctionalSubroutine = true // flag turns on defer func() { f.isFunctionalSubroutine = false diff --git a/formatter/declaration_format_test.go b/formatter/declaration_format_test.go index 405f20c6..84294f90 100644 --- a/formatter/declaration_format_test.go +++ b/formatter/declaration_format_test.go @@ -31,45 +31,22 @@ func TestAclDeclarationFormat(t *testing.T) { }, { name: "with comment", - input: `acl name { - "192.0.2.0"/24; // some comment - !"192.0.2.12"; + input: `acl /* before_name */name/* after_name */ { + // leading + "192.0.2.0"/24 /* before_semicolon */ ; // some comment + ! /* inside_inverse */ "192.0.2.12"; "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // The ending } // trailing`, - expect: `acl name { - "192.0.2.0"/24; // some comment - !"192.0.2.12"; + expect: `acl /* before_name */ name /* after_name */ { + // leading + "192.0.2.0"/24 /* before_semicolon */; // some comment + ! /* inside_inverse */ "192.0.2.12"; "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // The ending } // trailing `, }, - { - name: "align trailing comment", - input: `acl name { - "192.0.2.0"/24; // some comment - !"192.0.2.12"; // some comment - - "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // some comment - // The ending - } // trailing`, - expect: `acl name { - "192.0.2.0"/24; // some comment - !"192.0.2.12"; // some comment - - "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"; // some comment - // The ending -} // trailing -`, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - TrailingCommentWidth: 2, - LineWidth: 120, - AlignTrailingComment: true, - }, - }, } for _, tt := range tests { @@ -88,8 +65,10 @@ func TestBackendDeclarationFormat(t *testing.T) { }{ { name: "basic formatting", - input: `backend example { - .connect_timeout = 1s; + input: `// leading + backend /* before_name */ example /* after_name */ { + // leading + .connect_timeout /* after_name */ = /* before_value */ 1s /* after_value */; // trailing .dynamic = true; .port = "443"; .host = "example.com"; @@ -102,8 +81,10 @@ func TestBackendDeclarationFormat(t *testing.T) { .dummy = true; } }`, - expect: `backend example { - .connect_timeout = 1s; + expect: `// leading +backend /* before_name */ example /* after_name */ { + // leading + .connect_timeout /* after_name */ = /* before_value */ 1s /* after_value */; // trailing .dynamic = true; .port = "443"; .host = "example.com"; @@ -158,9 +139,9 @@ func TestBackendDeclarationFormat(t *testing.T) { }, }, { - name: "sorted and alignment properties", + name: "property alignment with comment", input: `backend example { - .connect_timeout = 1s; + .connect_timeout /* after_name */ = /* before_value */ 1s /* after_value */; // trailing .dynamic = true; .port = "443"; .host = "example.com"; @@ -174,17 +155,17 @@ func TestBackendDeclarationFormat(t *testing.T) { } }`, expect: `backend example { - .between_bytes_timeout = 30s; - .connect_timeout = 1s; - .dynamic = true; - .first_byte_timeout = 30s; - .host = "example.com"; - .max_connections = 500; - .port = "443"; - .ssl = true; - .probe = { - .dummy = true; + .connect_timeout /* after_name */ = /* before_value */ 1s /* after_value */; // trailing + .dynamic = true; + .port = "443"; + .host = "example.com"; + .first_byte_timeout = 30s; + .max_connections = 500; + .between_bytes_timeout = 30s; + .ssl = true; + .probe = { .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; + .dummy = true; } } `, @@ -192,21 +173,20 @@ func TestBackendDeclarationFormat(t *testing.T) { IndentWidth: 2, IndentStyle: "space", AlignDeclarationProperty: true, - SortDeclarationProperty: true, TrailingCommentWidth: 2, LineWidth: 80, }, }, { - name: "sorted and align properties and comments", + name: "sorted and alignment properties", input: `backend example { - .connect_timeout = 1s; // some comment + .connect_timeout = 1s; .dynamic = true; .port = "443"; .host = "example.com"; .first_byte_timeout = 30s; .max_connections = 500; - .between_bytes_timeout = 30s; // some comment + .between_bytes_timeout = 30s; .ssl = true; .probe = { .request = "GET / HTTP/1.1" "Host: example.com" "Connection: close"; @@ -214,8 +194,8 @@ func TestBackendDeclarationFormat(t *testing.T) { } }`, expect: `backend example { - .between_bytes_timeout = 30s; // some comment - .connect_timeout = 1s; // some comment + .between_bytes_timeout = 30s; + .connect_timeout = 1s; .dynamic = true; .first_byte_timeout = 30s; .host = "example.com"; @@ -234,8 +214,7 @@ func TestBackendDeclarationFormat(t *testing.T) { AlignDeclarationProperty: true, SortDeclarationProperty: true, TrailingCommentWidth: 2, - LineWidth: 120, - AlignTrailingComment: true, + LineWidth: 80, }, }, } @@ -256,16 +235,24 @@ func TestDirectorDeclarationFormat(t *testing.T) { }{ { name: "basic formatting", - input: `director example hash { - { .backend=F_backend1; .weight=1; } + input: `// leading + director /* before_name */ example /* after_name */ hash /* after_type */ { + .quorum = 50%; + // leading + { /* before_name */ .backend /* after_name */ = /* before_value */ F_backend1 /* after_value */; .weight=1; /* before_right_brace */ } { .backend=F_backend2; .weight=1; } { .backend=F_backend3; .weight=1; } - }`, - expect: `director example hash { - { .backend = F_backend1; .weight = 1; } + // infix + } // trailing`, + expect: `// leading +director /* before_name */ example /* after_name */ hash /* after_type */ { + .quorum = 50%; + // leading + { /* before_name */ .backend /* after_name */ = /* before_value */ F_backend1 /* after_value */; .weight = 1; /* before_right_brace */ } { .backend = F_backend2; .weight = 1; } { .backend = F_backend3; .weight = 1; } -} + // infix +} // trailing `, }, { @@ -286,34 +273,7 @@ func TestDirectorDeclarationFormat(t *testing.T) { IndentStyle: "space", SortDeclarationProperty: true, TrailingCommentWidth: 2, - LineWidth: 120, - }, - }, - { - name: "sort properties and align comment", - input: `director example random { - .retries = 1; // some comment - .quorum = 1; // some comment - { .weight=1; .backend=F_backend1; } - { .weight=1; .backend=F_backend2; } - { .weight=1; .backend=F_backend3; } - }`, - expect: `director example random { - .quorum = 1; // some comment - .retries = 1; // some comment - { .backend = F_backend1; .weight = 1; } - { .backend = F_backend2; .weight = 1; } - { .backend = F_backend3; .weight = 1; } -} -`, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - SortDeclarationProperty: true, - TrailingCommentWidth: 2, - LineWidth: 120, - AlignTrailingComment: true, - AlignDeclarationProperty: true, + LineWidth: 80, }, }, } @@ -334,26 +294,32 @@ func TestTableDeclarationFormat(t *testing.T) { }{ { name: "basic formatting", - input: `table routing_table BACKEND { - "a.example.com":F_backendA, + input: `// leading + table /* before_name */ routing_table /* after_name */ BACKEND /* after_type */ { + // leading + "a.example.com"/* after_key */:/* before_value */F_backendA /* after_value */, "b.example.com":F_backendB, "c.example.com":F_backendC, - }`, - expect: `table routing_table BACKEND { - "a.example.com": F_backendA, + // infix + } // trailing`, + expect: `// leading +table /* before_name */ routing_table /* after_name */ BACKEND /* after_type */ { + // leading + "a.example.com" /* after_key */: /* before_value */ F_backendA /* after_value */, "b.example.com": F_backendB, "c.example.com": F_backendC, -} + // infix +} // trailing `, }, { name: "basic formatting without table type", - input: `table routing_table { + input: `table routing_table /* after_name */ { "a.example.com": "foo", "b.example.com": "bar", "c.example.com": "baz", }`, - expect: `table routing_table { + expect: `table routing_table /* after_name */ { "a.example.com": "foo", "b.example.com": "bar", "c.example.com": "baz", @@ -382,7 +348,7 @@ func TestTableDeclarationFormat(t *testing.T) { }, }, { - name: "align properties", + name: "alignment properties", input: `table routing_table BACKEND { "a.example.com":F_backendA, "bb.example.com":F_backendB, @@ -402,28 +368,6 @@ func TestTableDeclarationFormat(t *testing.T) { LineWidth: 80, }, }, - { - name: "alig properties and comments", - input: `table routing_table BACKEND { - "a.example.com":F_backendA, - "bb.example.com":F_backendBB, // some comment - "ccc.example.com":F_backendCCC, // some comment - }`, - expect: `table routing_table BACKEND { - "a.example.com" : F_backendA, - "bb.example.com" : F_backendBB, // some comment - "ccc.example.com": F_backendCCC, // some comment -} -`, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - AlignDeclarationProperty: true, - TrailingCommentWidth: 2, - LineWidth: 120, - AlignTrailingComment: true, - }, - }, } for _, tt := range tests { @@ -442,10 +386,12 @@ func TestPenaltyboxDeclarationFormat(t *testing.T) { }{ { name: "formatting with comments", - input: `penaltybox banned_users { + input: `// leading + penaltybox /* before_name */ banned_users /* after_name */ { # no properties } // trailing comment`, - expect: `penaltybox banned_users { + expect: `// leading +penaltybox /* before_name */ banned_users /* after_name */ { # no properties } // trailing comment `, @@ -468,10 +414,12 @@ func TestRatecounterDeclarationFormat(t *testing.T) { }{ { name: "formatting with comments", - input: `ratecounter requests_rate { + input: `// leading + ratecounter /* before_name */ requests_rate /* after_name */ { # no properties } // trailing comment`, - expect: `ratecounter requests_rate { + expect: `// leading +ratecounter /* before_name */ requests_rate /* after_name */ { # no properties } // trailing comment `, @@ -495,37 +443,18 @@ func TestSubroutineDeclarationFormat(t *testing.T) { { name: "basic formatting with comments", input: `// subroutine leading comment -sub vcl_recv { +sub /* before_name */ vcl_recv /* after_name */ { // leading set req.http.Foo = "bar"; // subroutine infix comment } // subroutine trailing comment`, expect: `// subroutine leading comment -sub vcl_recv { +sub /* before_name */ vcl_recv /* after_name */ { + // leading set req.http.Foo = "bar"; // subroutine infix comment } // subroutine trailing comment `, }, - { - name: "align statements comment inside subroutine", - input: ` -sub vcl_recv { - set req.http.Foo = "bar"; // some comment - set req.htto.Lorem = "ipsum"; // some comment -}`, - expect: `sub vcl_recv { - set req.http.Foo = "bar"; // some comment - set req.htto.Lorem = "ipsum"; // some comment -} -`, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - TrailingCommentWidth: 2, - LineWidth: 120, - AlignTrailingComment: true, - }, - }, } for _, tt := range tests { @@ -535,7 +464,7 @@ sub vcl_recv { } } -func TestFunctionalSubroutne(t *testing.T) { +func TestFunctionalSubroutineDeclarationFormat(t *testing.T) { tests := []struct { name string input string @@ -543,13 +472,18 @@ func TestFunctionalSubroutne(t *testing.T) { conf *config.FormatConfig }{ { - name: "basic formartting", - input: `sub boolfn BOOL { - return true; -}`, - expect: `sub boolfn BOOL { - return true; -} + name: "basic formatting with comments", + input: `// subroutine leading comment +sub /* before_name */ foo /* after_name */ STRING /* after_type */ { // leading + return "BAR"; + // subroutine infix comment +} // subroutine trailing comment`, + expect: `// subroutine leading comment +sub /* before_name */ foo /* after_name */ STRING /* after_type */ { + // leading + return "BAR"; + // subroutine infix comment +} // subroutine trailing comment `, }, } @@ -562,6 +496,7 @@ func TestFunctionalSubroutne(t *testing.T) { } func TestComplicatedExpressions(t *testing.T) { + t.SkipNow() tests := []struct { name string input string diff --git a/formatter/expression_format.go b/formatter/expression_format.go index 04271516..30cc09b6 100644 --- a/formatter/expression_format.go +++ b/formatter/expression_format.go @@ -17,6 +17,7 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { buf.Write(v, Comment) } + var isSkipTrailing bool switch t := expr.(type) { // Primitive types return string case *ast.Ident: @@ -45,11 +46,16 @@ func (f *Formatter) formatExpression(expr ast.Expression) *ChunkBuffer { buf.Append(f.formatGroupedExpression(t)) case *ast.InfixExpression: buf.Append(f.formatInfixExpression(t)) + // Trick: infix expression trailing comment does not be needed to format + // because the Right expression also has the same trailing comment + isSkipTrailing = true } // trailing comment - if v := f.formatComment(expr.GetMeta().Trailing, "", 0); v != "" { - buf.Write(v, Comment) + if !isSkipTrailing { + if v := f.formatComment(expr.GetMeta().Trailing, "", 0); v != "" { + buf.Write(v, Comment) + } } return buf diff --git a/formatter/helper.go b/formatter/helper.go index 3ab55866..7d43343a 100644 --- a/formatter/helper.go +++ b/formatter/helper.go @@ -1,9 +1,11 @@ package formatter import ( + "bytes" "regexp" "strings" + "github.com/ysugimoto/falco/ast" "github.com/ysugimoto/falco/config" ) @@ -52,3 +54,27 @@ func formatCommentCharacter(comment string, char rune) string { return string(bs) } + +func isInlineComment(comments ast.Comments) bool { + if len(comments) == 0 { + return true + } + return strings.HasPrefix(comments[0].Value, "/*") +} + +func getLineOffset(b bytes.Buffer) int { + s := b.String() + if p := strings.LastIndex(s, "\n"); p >= 0 { + return len(s[p:]) + } + return len(s) +} + +func formatChunkedString(chunk string, indent string) string { + var buf bytes.Buffer + + for _, line := range strings.Split(chunk, "\n") { + buf.WriteString(indent + strings.TrimSpace(line) + "\n") + } + return buf.String() +} diff --git a/formatter/statement_format.go b/formatter/statement_format.go index 68c2c14c..c63e7472 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -2,7 +2,6 @@ package formatter import ( "bytes" - "fmt" "strings" "github.com/ysugimoto/falco/ast" @@ -46,9 +45,9 @@ func (f *Formatter) formatStatement(stmt ast.Statement) *Line { case *ast.SwitchStatement: line.Buffer += f.formatSwitchStatement(t) case *ast.RestartStatement: - line.Buffer += f.formatRestartStatement() + line.Buffer += f.formatRestartStatement(t) case *ast.EsiStatement: - line.Buffer += f.formatEsiStatement() + line.Buffer += f.formatEsiStatement(t) case *ast.AddStatement: line.Buffer += f.formatAddStatement(t) case *ast.CallStatement: @@ -95,7 +94,7 @@ func (f *Formatter) formatImportStatement(stmt *ast.ImportStatement) string { var buf bytes.Buffer buf.WriteString("import ") - buf.WriteString(stmt.Name.Value) + buf.WriteString(stmt.Name.String()) buf.WriteString(";") return buf.String() @@ -106,7 +105,7 @@ func (f *Formatter) formatIncludeStatement(stmt *ast.IncludeStatement) string { var buf bytes.Buffer buf.WriteString("include ") - buf.WriteString(f.formatString(stmt.Module)) + buf.WriteString(stmt.Module.String()) buf.WriteString(";") return buf.String() @@ -150,8 +149,12 @@ func (f *Formatter) formatBlockStatement(stmt *ast.BlockStatement) string { func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { var buf bytes.Buffer - buf.WriteString("declare local " + stmt.Name.Value) - buf.WriteString(" " + stmt.ValueType.Value) + buf.WriteString("declare ") + if v := f.formatComment(stmt.Infix, " ", 0); v != "" { + buf.WriteString(v) + } + buf.WriteString("local " + stmt.Name.String()) + buf.WriteString(" " + stmt.ValueType.String()) buf.WriteString(";") return buf.String() @@ -161,7 +164,7 @@ func (f *Formatter) formatDeclareStatement(stmt *ast.DeclareStatement) string { func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { var buf bytes.Buffer - buf.WriteString("set " + stmt.Ident.Value) + buf.WriteString("set " + stmt.Ident.String()) buf.WriteString(" " + stmt.Operator.Operator + " ") buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") @@ -173,7 +176,7 @@ func (f *Formatter) formatSetStatement(stmt *ast.SetStatement) string { func (f *Formatter) formatUnsetStatement(stmt *ast.UnsetStatement) string { var buf bytes.Buffer - buf.WriteString("unset " + stmt.Ident.Value) + buf.WriteString("unset " + stmt.Ident.String()) buf.WriteString(";") return buf.String() @@ -186,9 +189,9 @@ func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { // The "remove" statement is alias of "unset" statement, // so it could replaced to unset by configuration if f.conf.ShouldUseUnset { - buf.WriteString("unset " + stmt.Ident.Value) + buf.WriteString("unset " + stmt.Ident.String()) } else { - buf.WriteString("remove " + stmt.Ident.Value) + buf.WriteString("remove " + stmt.Ident.String()) } buf.WriteString(";") @@ -199,24 +202,38 @@ func (f *Formatter) formatRemoveStatement(stmt *ast.RemoveStatement) string { func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { var buf bytes.Buffer - buf.WriteString(stmt.Keyword + " (") + buf.WriteString(stmt.Keyword) + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(" (") // Condition expression chunk string may be printed with multi-lines. chunk := f.formatExpression(stmt.Condition).ChunkedString(stmt.Nest, stmt.Nest*f.conf.IndentWidth) if strings.Contains(chunk, "\n") { - buf.WriteString( - fmt.Sprintf( - "\n%s%s\n", - f.indent(stmt.Nest+1), - chunk, - ), - ) + buf.WriteString("\n") + buf.WriteString(formatChunkedString(chunk, f.indent(stmt.Nest+1))) buf.WriteString(f.indent(stmt.Nest) + ") ") } else { buf.WriteString(chunk + ") ") } + if v := f.formatComment(stmt.Consequence.Leading, " ", 0); v != "" { + buf.WriteString(v) + } + buf.WriteString(f.formatBlockStatement(stmt.Consequence)) + if v := f.formatComment(stmt.Consequence.Trailing, "", 0); v != "" { + // If comment is inline , concat to the same line + if isInlineComment(stmt.Consequence.Trailing) { + buf.WriteString(" " + v) + } else { + // Otherwise, print to the new line + buf.WriteString("\n") + buf.WriteString(f.indent(stmt.Consequence.Nest-1) + v) + buf.WriteString("\n") + } + } // else if, elseif, elsif for _, a := range stmt.Another { @@ -231,25 +248,39 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { buf.WriteString(" ") } + // buf.WriteString(f.formatElseIfStatement(a)) keyword := a.Keyword if f.conf.ElseIf { keyword = "else if" } - chunk := f.formatExpression(a.Condition).ChunkedString(a.Nest, a.Nest*f.conf.IndentWidth) - buf.WriteString(keyword + " (") + buf.WriteString(keyword) + if v := f.formatComment(a.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(" (") + chunk := f.formatExpression(a.Condition).ChunkedString(a.Nest, getLineOffset(buf)) if strings.Contains(chunk, "\n") { - buf.WriteString( - fmt.Sprintf( - "\n%s%s\n", - f.indent(a.Nest+1), - chunk, - ), - ) - buf.WriteString(f.indent(stmt.Nest) + ") ") + buf.WriteString("\n") + buf.WriteString(formatChunkedString(chunk, f.indent(a.Nest+1))) + buf.WriteString(f.indent(a.Nest) + ") ") } else { buf.WriteString(chunk + ") ") } + if v := f.formatComment(a.Consequence.Leading, "", 0); v != "" { + buf.WriteString(v + " ") + } buf.WriteString(f.formatBlockStatement(a.Consequence)) + if v := f.formatComment(a.Consequence.Trailing, "", 0); v != "" { + // If comment is inline , concat to the same line + if isInlineComment(a.Consequence.Trailing) { + buf.WriteString(" " + v) + } else { + // Otherwise, print to the new line + buf.WriteString("\n") + buf.WriteString(f.indent(a.Consequence.Nest-1) + v) + buf.WriteString("\n") + } + } } // else @@ -262,7 +293,11 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { buf.WriteString(" ") } buf.WriteString("else ") - buf.WriteString(f.formatBlockStatement(stmt.Alternative)) + if v := f.formatComment(stmt.Alternative.Infix, "", 0); v != "" { + buf.WriteString(v + " ") + } + buf.WriteString(f.formatBlockStatement(stmt.Alternative.Consequence)) + buf.WriteString(f.trailing(stmt.Alternative.Consequence.Trailing)) } return buf.String() @@ -272,7 +307,9 @@ func (f *Formatter) formatIfStatement(stmt *ast.IfStatement) string { func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { var buf bytes.Buffer - buf.WriteString("switch (" + f.formatExpression(stmt.Control).String() + ") {\n") + buf.WriteString("switch ") + buf.WriteString(strings.TrimSpace(stmt.Control.String())) + buf.WriteString(" {\n") for _, c := range stmt.Cases { // If indent_cale_labels is false, subtrat 1 nest level if !f.conf.IndentCaseLabels { @@ -283,14 +320,19 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { if c.Test != nil { buf.WriteString("case ") + if v := f.formatComment(c.Infix, " ", 0); v != "" { + buf.WriteString(v) + } if c.Test.Operator == "~" { buf.WriteString("~ ") } buf.WriteString(f.formatExpression(c.Test.Right).String()) - buf.WriteString(":\n") + buf.WriteString(":") } else { - buf.WriteString("default:\n") + buf.WriteString("default:") } + buf.WriteString(f.trailing(c.Trailing)) + buf.WriteString("\n") buf.WriteString(f.formatCaseSectionStatements(c)) } if len(stmt.Infix) > 0 { @@ -318,15 +360,17 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { lines = Lines{} } // need to plus 1 to nested indent because parser won't increase nest level - line := &Line{ - Leading: f.formatComment(stmt.GetMeta().Leading, "\n", meta.Nest+1), - Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", meta.Nest+1), - } - if _, ok := stmt.(*ast.BreakStatement); ok { - line.Buffer = f.indent(meta.Nest+1) + "break;" - } else { - line.Buffer = f.formatStatement(stmt).String() - } + stmt.GetMeta().Nest++ + line := f.formatStatement(stmt) + // line := &Line{ + // Leading: f.formatComment(stmt.GetMeta().Leading, "\n", meta.Nest+1), + // Trailing: f.formatComment(stmt.GetMeta().Trailing, "\n", meta.Nest+1), + // } + // if _, ok := stmt.(*ast.BreakStatement); ok { + // line.Buffer = f.indent(meta.Nest+1) + "break;" + // } else { + // line.Buffer = f.formatStatement(stmt).String() + // } lines = append(lines, line) } @@ -349,19 +393,27 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { } // Format restart statement -func (f *Formatter) formatRestartStatement() string { +func (f *Formatter) formatRestartStatement(stmt *ast.RestartStatement) string { var buf bytes.Buffer - buf.WriteString("restart;") + buf.WriteString("restart") + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(";") return buf.String() } // Format esi statement -func (f *Formatter) formatEsiStatement() string { +func (f *Formatter) formatEsiStatement(stmt *ast.EsiStatement) string { var buf bytes.Buffer - buf.WriteString("esi;") + buf.WriteString("esi") + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(";") return buf.String() } @@ -370,7 +422,7 @@ func (f *Formatter) formatEsiStatement() string { func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { var buf bytes.Buffer - buf.WriteString("add " + stmt.Ident.Value) + buf.WriteString("add " + stmt.Ident.String()) buf.WriteString(" " + stmt.Operator.Operator + " ") buf.WriteString(f.formatExpression(stmt.Value).ChunkedString(stmt.Nest, buf.Len())) buf.WriteString(";") @@ -382,7 +434,7 @@ func (f *Formatter) formatAddStatement(stmt *ast.AddStatement) string { func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { var buf bytes.Buffer - buf.WriteString("call " + stmt.Subroutine.Value) + buf.WriteString("call " + stmt.Subroutine.String()) buf.WriteString(";") return buf.String() @@ -392,10 +444,10 @@ func (f *Formatter) formatCallStatement(stmt *ast.CallStatement) string { func (f *Formatter) formatErrorStatement(stmt *ast.ErrorStatement) string { var buf bytes.Buffer - buf.WriteString("error " + f.formatExpression(stmt.Code).String()) + buf.WriteString("error " + stmt.Code.String()) // argument is arbitrary if stmt.Argument != nil { - buf.WriteString(" " + f.formatExpression(stmt.Argument).String()) + buf.WriteString(" " + stmt.Argument.String()) } buf.WriteString(";") @@ -419,6 +471,9 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { buf.WriteString("return") if stmt.ReturnExpression != nil { + if v := f.formatComment(stmt.ParenthesisLeadingComments, "", 0); v != "" { + buf.WriteString(" " + v) + } prefix := " " suffix := "" // If ReturnStatementParenthesis is enabled and inside functional subroutine, @@ -428,9 +483,17 @@ func (f *Formatter) formatReturnStatement(stmt *ast.ReturnStatement) string { suffix = ")" } buf.WriteString(prefix) - buf.WriteString(f.formatExpression(*stmt.ReturnExpression).String()) + buf.WriteString(stmt.ReturnExpression.String()) buf.WriteString(suffix) + if v := f.formatComment(stmt.ParenthesisTrailingComments, "", 0); v != "" { + buf.WriteString(" " + v) + } + } else { + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } } + buf.WriteString(";") return buf.String() @@ -462,7 +525,7 @@ func (f *Formatter) formatSyntheticBase64Statement(stmt *ast.SyntheticBase64Stat func (f *Formatter) formatGotoStatement(stmt *ast.GotoStatement) string { var buf bytes.Buffer - buf.WriteString("goto " + stmt.Destination.Value) + buf.WriteString("goto " + stmt.Destination.String()) buf.WriteString(";") return buf.String() @@ -489,7 +552,11 @@ func (f *Formatter) formatFunctionCallStatement(stmt *ast.FunctionCallStatement) buf.WriteString(", ") } } - buf.WriteString(");") + buf.WriteString(")") + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(";") return buf.String() } diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index 15ccabb5..fae50c4f 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -16,10 +16,10 @@ func TestFormatImportStatement(t *testing.T) { { name: "basic formatting with comments", input: `// import leading comment - import boltsort; // import trailing comment + import /* before_name */ boltsort /* after_name */ ; // import trailing comment `, expect: `// import leading comment -import boltsort; // import trailing comment +import /* before_name */ boltsort /* after_name */; // import trailing comment `, }, } @@ -41,19 +41,19 @@ func TestFormatIncludeStatement(t *testing.T) { { name: "basic formatting with comments", input: `// include leading comment - include "feature_mod"; // include trailing comment + include /* before_name */ "feature_mod" /* after_name */ ; // include trailing comment `, expect: `// include leading comment -include "feature_mod"; // include trailing comment +include /* before_name */ "feature_mod" /* after_name */; // include trailing comment `, }, { name: "basic formatting without semicolon", input: `// include leading comment - include "feature_mod" // include trailing comment + include /* before_name */ "feature_mod" // include trailing comment `, expect: `// include leading comment -include "feature_mod"; // include trailing comment +include /* before_name */ "feature_mod"; // include trailing comment `, }, { @@ -141,12 +141,12 @@ func TestFormatDeclareStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // declare leading comment - declare local var.FOO STRING ; // declare trailing comment + declare /* before_local */ local /* after_local */ var.FOO /* after_name */ STRING /* after_type */ ; // declare trailing comment } `, expect: `sub vcl_recv { // declare leading comment - declare local var.FOO STRING; // declare trailing comment + declare /* before_local */ local /* after_local */ var.FOO /* after_name */ STRING /* after_type */; // declare trailing comment } `, }, @@ -170,12 +170,13 @@ func TestFormatSetStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // set leading comment - set req.http.Foo= "foo" "bar" "baz" ; // set trailing comment + set /* before_ident */ req.http.Foo/* after_ident */= /* before_value1 */"foo" /* after_value1 */ "bar" "baz" /* before_semicolon */ ; // set trailing comment } `, expect: `sub vcl_recv { // set leading comment - set req.http.Foo = "foo" "bar" "baz"; // set trailing comment + set /* before_ident */ req.http.Foo /* after_ident */ = /* before_value1 */ "foo" /* after_value1 */ "bar" "baz" + /* before_semicolon */; // set trailing comment } `, }, @@ -199,12 +200,12 @@ func TestFormatUnsetStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // unset leading comment - unset req.http.Foo ; // unset trailing comment + unset /* before_ident */req.http.Foo /* after_ident */ ; // unset trailing comment } `, expect: `sub vcl_recv { // unset leading comment - unset req.http.Foo; // unset trailing comment + unset /* before_ident */ req.http.Foo /* after_ident */; // unset trailing comment } `, }, @@ -228,12 +229,12 @@ func TestFormatRemoveStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // remove leading comment - remove req.http.Foo ; // remove trailing comment + remove /* before_ident */req.http.Foo /* after_ident */ ; // remove trailing comment } `, expect: `sub vcl_recv { // remove leading comment - remove req.http.Foo; // remove trailing comment + remove /* before_ident */ req.http.Foo /* after_ident */; // remove trailing comment } `, }, @@ -257,12 +258,12 @@ func TestFormatRestartStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // restart leading comment - restart ; // restart trailing comment + restart /* infix_comment */ ; // restart trailing comment } `, expect: `sub vcl_recv { // restart leading comment - restart; // restart trailing comment + restart /* infix_comment */; // restart trailing comment } `, }, @@ -287,13 +288,13 @@ func TestFormatEsitStatement(t *testing.T) { input: `sub vcl_recv { // esi leading comment // esi leading comment - esi ; // esi trailing comment + esi /* infix_comment */ ; // esi trailing comment } `, expect: `sub vcl_recv { // esi leading comment // esi leading comment - esi; // esi trailing comment + esi /* infix_comment */; // esi trailing comment } `, }, @@ -317,12 +318,12 @@ func TestFormatAddStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_deliver { // add leading comment - add resp.http.Set-Cookie= "name=foo" ; // add trailing comment + add /* before_ident */resp.http.Set-Cookie /* after_ident */= /* before_value */"name=foo" /* after_value */ ; // add trailing comment } `, expect: `sub vcl_deliver { // add leading comment - add resp.http.Set-Cookie = "name=foo"; // add trailing comment + add /* before_ident */ resp.http.Set-Cookie /* after_ident */ = /* before_value */ "name=foo" /* after_value */; // add trailing comment } `, }, @@ -346,12 +347,12 @@ func TestFormatCallStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // call leading comment - call feature_recv ; // call trailing comment + call /* before_subroutine */feature_recv /* after_subroutine */ ; // call trailing comment } `, expect: `sub vcl_recv { // call leading comment - call feature_recv; // call trailing comment + call /* before_subroutine */ feature_recv /* after_subroutine */; // call trailing comment } `, }, @@ -375,12 +376,12 @@ func TestFormatErrorStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // error leading comment - error 404 ; // error trailing comment + error /* before_code */ 404 /* after_code */; // error trailing comment } `, expect: `sub vcl_recv { // error leading comment - error 404; // error trailing comment + error /* before_code */ 404 /* after_code */; // error trailing comment } `, }, @@ -388,12 +389,12 @@ func TestFormatErrorStatement(t *testing.T) { name: "formatting with response", input: `sub vcl_recv { // error leading comment - error 404 "extra response" ; // error trailing comment + error /* before_code */ 404 /* after_code */ "extra response" /* after_response */ ; // error trailing comment } `, expect: `sub vcl_recv { // error leading comment - error 404 "extra response"; // error trailing comment + error /* before_code */ 404 /* after_code */ "extra response" /* after_response */; // error trailing comment } `, }, @@ -429,12 +430,27 @@ func TestFormatLogStatement(t *testing.T) { { name: "multiple expressions", input: `sub vcl_log { - log req.http.Host "foo" "bar" "baz" client.ip ; + log /* before_value1 */ req.http.Host /* after_value1 */ "foo" /* after_value2 */"bar" /* after_value3 */ "baz" /* after_value4 */ client.ip /* after_value5 */; } `, expect: `sub vcl_log { - log req.http.Host "foo" "bar" "baz" client.ip; + log /* before_value1 */ req.http.Host /* after_value1 */ "foo" /* after_value2 */ "bar" /* after_value3 */ "baz" + /* after_value4 */ client.ip /* after_value5 */; +} +`, + }, + { + name: "multiple expressions including line comment", + input: `sub vcl_log { + log req.http.Host // request Host + "foo" /* after_value2 */"bar" ; + +} +`, + expect: `sub vcl_log { + log req.http.Host // request Host + "foo" /* after_value2 */ "bar"; } `, }, @@ -458,23 +474,23 @@ func TestFormatReturnStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // return leading comment - return lookup ; // return trailing comment + return /* before_state */ lookup /* after_state */; // return trailing comment } `, expect: `sub vcl_recv { // return leading comment - return lookup; // return trailing comment + return /* before_state */ lookup /* after_state */; // return trailing comment } `, }, { name: "with parenthesis", input: `sub vcl_recv { - return(lookup) ; + return /* before_parenthesis */ (/* inside_parenthesis */ lookup /* inside_parenthesis */) /* after_parenthesis */; } `, expect: `sub vcl_recv { - return (lookup); + return /* before_parenthesis */ (/* inside_parenthesis */ lookup /* inside_parenthesis */) /* after_parenthesis */; } `, conf: &config.FormatConfig{ @@ -487,11 +503,11 @@ func TestFormatReturnStatement(t *testing.T) { { name: "without argument", input: `sub vcl_recv { - return ; + return /* before_semicolon */ ; // trailing } `, expect: `sub vcl_recv { - return; + return /* before_semicolon */; // trailing } `, }, @@ -532,28 +548,28 @@ func TestFormatSynthticStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_error { // synthetic leading comment - synthetic "foo" ; // synthetic trailing comment + synthetic /* before_value */ "foo" /* after_value */ ; // synthetic trailing comment } `, expect: `sub vcl_error { // synthetic leading comment - synthetic "foo"; // synthetic trailing comment + synthetic /* before_value */ "foo" /* after_value */; // synthetic trailing comment } `, }, { name: "with bracket string", input: `sub vcl_error { - synthetic {"foo bar baz"} ; + synthetic /* before_value */ {"foo bar baz"} /* after_value */; } `, expect: `sub vcl_error { - synthetic {"foo bar baz"}; + synthetic /* before_value */ {"foo bar baz"} /* after_value */; } `, }, { - name: "with multipel expressions", + name: "with multiple expressions", input: `sub vcl_error { synthetic {"foo bar baz"} "lorem" "ipsum" req.http.Hoost ; } @@ -583,12 +599,13 @@ func TestFormatSynthticBase64Statement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_error { // synthetic.base64 leading comment - synthetic.base64 "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo=" ; // synthetic.base64 trailing comment + synthetic.base64 /* before_value */ "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo=" /* after_value */ ; // synthetic.base64 trailing comment } `, expect: `sub vcl_error { // synthetic.base64 leading comment - synthetic.base64 "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo="; // synthetic.base64 trailing comment + synthetic.base64 /* before_value */ "ZmFsY28gaXMgdGhlIGJldHRlciB0b29sIHRvIGRldmVsb3AgRmFzdGx5IFZDTAo=" + /* after_value */; // synthetic.base64 trailing comment } `, conf: &config.FormatConfig{ @@ -619,12 +636,12 @@ func TestFormatGotoStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // goto leading comment - goto FOO ; // goto trailing comment + goto /* before_target */ FOO /* after_target */; // goto trailing comment } `, expect: `sub vcl_recv { // goto leading comment - goto FOO; // goto trailing comment + goto /* before_target */ FOO /* after_target */; // goto trailing comment } `, }, @@ -679,12 +696,12 @@ func TestFormatFunctionCallStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // function call leading comment - h3.alt_svc(); // function call trailing comment + h3.alt_svc() /* infix_comment */; // function call trailing comment } `, expect: `sub vcl_recv { // function call leading comment - h3.alt_svc(); // function call trailing comment + h3.alt_svc() /* infix_comment */; // function call trailing comment } `, }, @@ -708,24 +725,28 @@ func TestFormatIfStatement(t *testing.T) { name: "basic formatting with comments", input: `sub vcl_recv { // if leading comment - if (req.http.Host) { + if /* if_infix */ (/* before_condition */ req.http.Host /* after_condition */) /* before_parenthesis */ { set req.http.Foo = req.http.Host; // if infix comment - } else { + } /* else_leading */ + // else leading + else /* else_infix */ { set req.http.Foo = "unknown"; // else infix comment - } // if trailing comment + } // else trailing comment } `, expect: `sub vcl_recv { // if leading comment - if (req.http.Host) { + if /* if_infix */ (/* before_condition */ req.http.Host /* after_condition */) /* before_parenthesis */ { set req.http.Foo = req.http.Host; // if infix comment - } else { + } /* else_leading */ + // else leading + else /* else_infix */ { set req.http.Foo = "unknown"; // else infix comment - } // if trailing comment + } // else trailing comment } `, }, @@ -736,7 +757,7 @@ func TestFormatIfStatement(t *testing.T) { if (req.http.Host) { set req.http.Foo = req.http.Host; // if infix comment - } else if (req.http.AnotherHost) { + } /* elseif_leading */ else if /* elseif_infix */ (/* elseif_condition_leading */ req.http.AnotherHost /* elseif_condition_trailing */) /* else_before_parenthesis */ { set req.http.Foo = "another"; } // More complecated case @@ -753,7 +774,10 @@ func TestFormatIfStatement(t *testing.T) { if (req.http.Host) { set req.http.Foo = req.http.Host; // if infix comment - } else if (req.http.AnotherHost) { + } /* elseif_leading */ else if /* elseif_infix */ ( + /* elseif_condition_leading */ req.http.AnotherHost + /* elseif_condition_trailing */ + ) /* else_before_parenthesis */ { set req.http.Foo = "another"; } // More complecated case @@ -787,7 +811,7 @@ func TestFormatIfStatement(t *testing.T) { IndentWidth: 2, IndentStyle: "space", TrailingCommentWidth: 2, - LineWidth: 70, + LineWidth: 80, }, }, } @@ -799,7 +823,7 @@ func TestFormatIfStatement(t *testing.T) { } } -func TestFormatSwitchStatement(t *testing.T) { +func TestSwitchStatement(t *testing.T) { tests := []struct { name string input string @@ -807,95 +831,38 @@ func TestFormatSwitchStatement(t *testing.T) { conf *config.FormatConfig }{ { - name: "basic formatting with comments", - input: `sub vcl_recv { - // switch leading comment - switch(req.http.Foo) { - // first case comment - case "bar": - // break leading comment - break; - // second case comment - case "baz": - // break leading comment - break; - // default case comment - default: - break; - // switch infix comment - } // trailing comment -} -`, - expect: `sub vcl_recv { - // switch leading comment - switch (req.http.Foo) { - // first case comment - case "bar": - // break leading comment - break; - // second case comment - case "baz": - // break leading comment - break; - // default case comment - default: - break; - // switch infix comment - } // trailing comment -} -`, - }, - { - name: "indent case labels", + name: "basic formatting", input: `sub vcl_recv { - // switch leading comment - switch(req.http.Foo) { - // first case comment - case "bar": - // break leading comment - break; - // second case comment - case "baz": - // break leading comment - break; - // default case comment - default: - break; - // switch infix comment - } // trailing comment + switch /* infix */ (/* before_control */ req.http.Host /* after_control */) /* before_block */ { + // case leading + case /* before_value */ "foo" /* after_value */: // case trailing + set req.http.Host = "bar"; + // fallthrough leading + fallthrough /* fallthrough_infix */ ; // fallthrough trailing + default /* default_infix */: // default trailing + // break leading + break /* break_infix */; // break trailing + } // switch trailing } `, expect: `sub vcl_recv { - // switch leading comment - switch (req.http.Foo) { - // first case comment - case "bar": - // break leading comment - break; - // second case comment - case "baz": - // break leading comment - break; - // default case comment - default: - break; - // switch infix comment - } // trailing comment + switch /* infix */ (/* before_control */ req.http.Host /* after_control */) /* before_block */ { + // case leading + case /* before_value */ "foo" /* after_value */: // case trailing + set req.http.Host = "bar"; + // fallthrough leading + fallthrough /* fallthrough_infix */ ; // fallthrough trailing + default /* default_infix */: // default trailing + // break leading + break /* break_infix */; // break trailing + } // switch trailing } `, - conf: &config.FormatConfig{ - IndentWidth: 2, - IndentStyle: "space", - TrailingCommentWidth: 2, - LineWidth: 70, - IndentCaseLabels: true, - }, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert(t, tt.input, tt.expect, tt.conf) + println(assert(t, tt.input, tt.expect, tt.conf)) }) } } diff --git a/go.mod b/go.mod index c6b0ef79..ed7108fd 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect diff --git a/go.sum b/go.sum index 924b3d51..27ee6803 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/parser/declaration_parser.go b/parser/declaration_parser.go index c7d42953..16075586 100644 --- a/parser/declaration_parser.go +++ b/parser/declaration_parser.go @@ -456,12 +456,18 @@ func (p *Parser) parseSubroutineDeclaration() (*ast.SubroutineDeclaration, error // https://developer.fastly.com/reference/vcl/subroutines/ // we dont need to validate the type here, linter will do that later. if p.expectPeek(token.IDENT) { + swapLeadingTrailing(p.curToken, s.Name.Meta) s.ReturnType = p.parseIdent() } if !p.expectPeek(token.LEFT_BRACE) { return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE")) } + if s.ReturnType != nil { + swapLeadingTrailing(p.curToken, s.ReturnType.Meta) + } else { + swapLeadingTrailing(p.curToken, s.Name.Meta) + } var err error if s.Block, err = p.parseBlockStatement(); err != nil { @@ -485,6 +491,7 @@ func (p *Parser) parsePenaltyboxDeclaration() (*ast.PenaltyboxDeclaration, error if !p.expectPeek(token.LEFT_BRACE) { return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE")) } + swapLeadingTrailing(p.curToken, pb.Name.Meta) var err error if pb.Block, err = p.parseBlockStatement(); err != nil { @@ -507,6 +514,7 @@ func (p *Parser) parseRatecounterDeclaration() (*ast.RatecounterDeclaration, err if !p.expectPeek(token.LEFT_BRACE) { return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE")) } + swapLeadingTrailing(p.curToken, r.Name.Meta) var err error if r.Block, err = p.parseBlockStatement(); err != nil { diff --git a/parser/parser_test.go b/parser/parser_test.go index c29407c9..4d69e883 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -276,11 +276,11 @@ sub /* subroutine ident leading */ vcl_recv /* subroutine block leading */ { &ast.SubroutineDeclaration{ Meta: ast.New(T, 0, comments("// subroutine leading")), Name: &ast.Ident{ - Meta: ast.New(T, 0, comments("/* subroutine ident leading */")), + Meta: ast.New(T, 0, comments("/* subroutine ident leading */"), comments("/* subroutine block leading */")), Value: "vcl_recv", }, Block: &ast.BlockStatement{ - Meta: ast.New(T, 1, comments("/* subroutine block leading */"), comments("// subroutine trailing"), comments("// subroutine block infix")), + Meta: ast.New(T, 1, comments(), comments("// subroutine trailing"), comments("// subroutine block infix")), Statements: []ast.Statement{ &ast.IfStatement{ Meta: ast.New(T, 1, comments("// if leading")), diff --git a/parser/statement_parser.go b/parser/statement_parser.go index af6afba7..4f7ce282 100644 --- a/parser/statement_parser.go +++ b/parser/statement_parser.go @@ -68,6 +68,10 @@ func (p *Parser) parseStatement() (ast.Statement, error) { } else { // Could be a goto destination stmt, err = p.parseGotoDestination() + if err != nil { + // raise an error on the current token + err = UnexpectedToken(p.curToken) + } } default: err = UnexpectedToken(p.curToken) @@ -457,9 +461,9 @@ func (p *Parser) parseReturnStatement() (*ast.ReturnStatement, error) { // return statement may not have argument // https://developer.fastly.com/reference/vcl/statements/return/ if p.peekTokenIs(token.SEMICOLON) { - stmt.Meta.Trailing = p.trailing() p.nextToken() // point to SEMICOLON - swapLeadingTrailing(p.curToken, stmt.Meta) + swapLeadingInfix(p.curToken, stmt.Meta) + stmt.Meta.Trailing = p.trailing() return stmt, nil } @@ -621,7 +625,7 @@ func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { // Note: VCL could define "else if" statement with "elseif", "elsif" keyword case token.ELSEIF, token.ELSIF: // elseif, elsif p.nextToken() // point to ELSEIF/ELSIF - another, err := p.parseAnotherIfStatement(p.curToken.Token.Literal) + another, err := p.parseAnotherIfStatement(p.peekToken.Token.Literal) if err != nil { return nil, errors.WithStack(err) } From 55668b9569716dd7bacbbe5151f46f324b0a87e0 Mon Sep 17 00:00:00 2001 From: ysugimoto Date: Tue, 30 Apr 2024 13:02:46 +0900 Subject: [PATCH 22/22] fix tests with complex comments --- formatter/chunk_buffer.go | 95 ++++++++++++++++-------------- formatter/helper.go | 5 +- formatter/statement_format.go | 44 ++++++++++++-- formatter/statement_format_test.go | 37 ++++++++++-- 4 files changed, 125 insertions(+), 56 deletions(-) diff --git a/formatter/chunk_buffer.go b/formatter/chunk_buffer.go index 7bb9f01b..39fc01bc 100644 --- a/formatter/chunk_buffer.go +++ b/formatter/chunk_buffer.go @@ -76,11 +76,11 @@ func (c *ChunkBuffer) nextChunk() *Chunk { } // Get peek chunk -func (c *ChunkBuffer) peekChunk() *Chunk { - if c.index+1 > len(c.chunks)-1 { +func (c *ChunkBuffer) peekChunk(i int) *Chunk { + if c.index+i > len(c.chunks)-1 { return nil } - return c.chunks[c.index+1] + return c.chunks[c.index+i] } // Append buffers @@ -175,33 +175,68 @@ func (c *ChunkBuffer) ChunkedString(level, offset int) string { } // infix operator case Infix: - // Or, the operator is the member of mustSingleOperators, the expression must be printed on the same line - // if _, ok := mustSingleOperators[chunk.buffer]; ok { - // buf.WriteString(c.chunkInfixOperator(state, chunk)) - // continue - // } buf.WriteString(c.chunkString(state, chunk.buffer)) // Otherwise (token), create chunk string default: - chunk.buffer += c.combineInfixChunk(chunk.buffer) + // Pre-combine infix operator that must be placed on the same line + chunk.buffer += c.combineInfixChunk() buf.WriteString(c.chunkString(state, chunk.buffer)) } } } -func (c *ChunkBuffer) combineInfixChunk(b string) string { - peek := c.peekChunk() - if peek == nil || peek.Type != Infix { - return "" +// Read peek chunk and combine if the chunk is placed on the same line +func (c *ChunkBuffer) combineInfixChunk() string { + var peek *Chunk + var expr string + var index int + + for { + index++ + peek = c.peekChunk(index) + if peek == nil { + return "" + } + switch peek.Type { + // If peek chunk is Infix, it may combine + case Infix: + goto OUT + // If peek chunk is Comment, should be combined and look up next chunk + case Comment: + expr += " " + peek.buffer + default: + return "" + } } +OUT: + // Infix operator _, ok := mustSingleOperators[peek.buffer] if !ok { return "" } - expr := " " + c.nextChunk().buffer - if next := c.nextChunk(); next != nil { - expr += " " + next.buffer + expr += " " + peek.buffer + + // Skip infix comments + for { + index++ + peek = c.peekChunk(index) + if peek == nil { + return "" + } + if peek.Type == Comment { + expr += " " + peek.buffer + continue + } + break + } + // Finally, add token buffer + expr += " " + peek.buffer + + // Forward index position to be read + for index > 0 { + c.nextChunk() + index-- } return expr @@ -253,34 +288,6 @@ func (c *ChunkBuffer) chunkGroupOperator(state *ChunkState, chunk *Chunk) string } } -// chunkInfixOperator() returns chunk infix expression string -func (c *ChunkBuffer) chunkInfixOperator(state *ChunkState, chunk *Chunk) string { - expr := chunk.buffer - - for { - next := c.nextChunk() - if next == nil { - state.count += len(expr) - return expr - } - - switch next.Type { - case Comment: - if next.isLineComment() { - expr += next.buffer - expr += c.nextLine(state) - state.reset() - continue - } - expr += " " + next.buffer - default: - expr += " " + next.buffer - state.count += len(expr) - return expr - } - } -} - // chunkString() returns chunked string func (c *ChunkBuffer) chunkString(state *ChunkState, expr string) string { var buf bytes.Buffer diff --git a/formatter/helper.go b/formatter/helper.go index 7d43343a..d8778e96 100644 --- a/formatter/helper.go +++ b/formatter/helper.go @@ -55,6 +55,7 @@ func formatCommentCharacter(comment string, char rune) string { return string(bs) } +// Return comment is inline comment that has "/* ... */" syntax func isInlineComment(comments ast.Comments) bool { if len(comments) == 0 { return true @@ -62,6 +63,7 @@ func isInlineComment(comments ast.Comments) bool { return strings.HasPrefix(comments[0].Value, "/*") } +// Get latest line offset (character length) from current buffer func getLineOffset(b bytes.Buffer) int { s := b.String() if p := strings.LastIndex(s, "\n"); p >= 0 { @@ -70,7 +72,8 @@ func getLineOffset(b bytes.Buffer) int { return len(s) } -func formatChunkedString(chunk string, indent string) string { +// Format multiple line chunk string with specified indent +func formatChunkedString(chunk, indent string) string { var buf bytes.Buffer for _, line := range strings.Split(chunk, "\n") { diff --git a/formatter/statement_format.go b/formatter/statement_format.go index c63e7472..d54dcebd 100644 --- a/formatter/statement_format.go +++ b/formatter/statement_format.go @@ -68,6 +68,10 @@ func (f *Formatter) formatStatement(stmt ast.Statement) *Line { line.Buffer += f.formatGotoDestinationStatement(t) case *ast.FunctionCallStatement: line.Buffer += f.formatFunctionCallStatement(t) + case *ast.BreakStatement: + line.Buffer += f.formatBreakStatement(t) + case *ast.FallthroughStatement: + line.Buffer += f.formatFallthroughStatement(t) // On if statement, trailing comment node depends on its declarations case *ast.IfStatement: @@ -329,7 +333,11 @@ func (f *Formatter) formatSwitchStatement(stmt *ast.SwitchStatement) string { buf.WriteString(f.formatExpression(c.Test.Right).String()) buf.WriteString(":") } else { - buf.WriteString("default:") + buf.WriteString("default") + if v := f.formatComment(c.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(":") } buf.WriteString(f.trailing(c.Trailing)) buf.WriteString("\n") @@ -384,14 +392,40 @@ func (f *Formatter) formatCaseSectionStatements(cs *ast.CaseStatement) string { var buf bytes.Buffer buf.WriteString(group.String()) - if cs.Fallthrough { - buf.WriteString(f.indent(cs.Meta.Nest + 1)) - buf.WriteString("fallthrough;") - } + // if cs.Fallthrough { + // buf.WriteString(f.indent(cs.Meta.Nest + 1)) + // buf.WriteString("fallthrough;") + // } return trimMutipleLineFeeds(buf.String()) } +// Format break statement +func (f *Formatter) formatBreakStatement(stmt *ast.BreakStatement) string { + var buf bytes.Buffer + + buf.WriteString("break") + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(";") + + return buf.String() +} + +// Format fallthrough statement +func (f *Formatter) formatFallthroughStatement(stmt *ast.FallthroughStatement) string { + var buf bytes.Buffer + + buf.WriteString("fallthrough") + if v := f.formatComment(stmt.Infix, "", 0); v != "" { + buf.WriteString(" " + v) + } + buf.WriteString(";") + + return buf.String() +} + // Format restart statement func (f *Formatter) formatRestartStatement(stmt *ast.RestartStatement) string { var buf bytes.Buffer diff --git a/formatter/statement_format_test.go b/formatter/statement_format_test.go index fae50c4f..ab999840 100644 --- a/formatter/statement_format_test.go +++ b/formatter/statement_format_test.go @@ -806,6 +806,31 @@ func TestFormatIfStatement(t *testing.T) { set req.http.OK = "1"; } } +`, + conf: &config.FormatConfig{ + IndentWidth: 2, + IndentStyle: "space", + TrailingCommentWidth: 2, + LineWidth: 80, + }, + }, + { + name: "chunked condition format with infix comments", + input: `sub vcl_recv { + if (req.http.Header1 == "1" && req.http.Header2 /* comment */ == /* comment */ "2" && req.http.Header3 == "3") { + set req.http.OK = "1"; + } +} +`, + expect: `sub vcl_recv { + if ( + req.http.Header1 == "1" && + req.http.Header2 /* comment */ == /* comment */ "2" && + req.http.Header3 == "3" + ) { + set req.http.OK = "1"; + } +} `, conf: &config.FormatConfig{ IndentWidth: 2, @@ -848,21 +873,21 @@ func TestSwitchStatement(t *testing.T) { expect: `sub vcl_recv { switch /* infix */ (/* before_control */ req.http.Host /* after_control */) /* before_block */ { // case leading - case /* before_value */ "foo" /* after_value */: // case trailing + case /* before_value */ "foo" /* after_value */: // case trailing set req.http.Host = "bar"; // fallthrough leading - fallthrough /* fallthrough_infix */ ; // fallthrough trailing - default /* default_infix */: // default trailing + fallthrough /* fallthrough_infix */; // fallthrough trailing + default /* default_infix */: // default trailing // break leading - break /* break_infix */; // break trailing - } // switch trailing + break /* break_infix */; // break trailing + } // switch trailing } `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - println(assert(t, tt.input, tt.expect, tt.conf)) + assert(t, tt.input, tt.expect, tt.conf) }) } }