From c266e7e5bf2e927d07b4dbab3ab1fcd9ada56cd1 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Fri, 11 Aug 2023 22:14:18 +0200 Subject: [PATCH 1/2] Add canonicalize ast pass to jsonnetfmt. --- cmd/jsonnetfmt/cmd.go | 3 + internal/formatter/canonicalize.go | 117 +++++++++++++++++++++++++++++ internal/formatter/jsonnetfmt.go | 7 ++ internal/formatter/unparser.go | 3 + 4 files changed, 130 insertions(+) create mode 100644 internal/formatter/canonicalize.go diff --git a/cmd/jsonnetfmt/cmd.go b/cmd/jsonnetfmt/cmd.go index 88420e31..316aa621 100644 --- a/cmd/jsonnetfmt/cmd.go +++ b/cmd/jsonnetfmt/cmd.go @@ -60,6 +60,7 @@ func usage(o io.Writer) { fmt.Fprintln(o, " --[no-]sort-imports Sorting of imports (on by default)") fmt.Fprintln(o, " --[no-]use-implicit-plus Remove plus signs where they are not required") fmt.Fprintln(o, " (on by default)") + fmt.Fprintln(o, " --canonicalize Generate a canonical output") fmt.Fprintln(o, " --version Print version") fmt.Fprintln(o) fmt.Fprintln(o, "In all cases:") @@ -181,6 +182,8 @@ func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArg config.options.PadObjects = false } else if arg == "--sort-imports" { config.options.SortImports = true + } else if arg == "--canonicalize" { + config.options.Canonicalize = true } else if arg == "--no-sort-imports" { config.options.SortImports = false } else if arg == "-c" || arg == "--create-output-dirs" { diff --git a/internal/formatter/canonicalize.go b/internal/formatter/canonicalize.go new file mode 100644 index 00000000..fc072f38 --- /dev/null +++ b/internal/formatter/canonicalize.go @@ -0,0 +1,117 @@ +package formatter + +import ( + "github.com/google/go-jsonnet/ast" + "github.com/google/go-jsonnet/internal/pass" + "sort" +) + +// Canonicalize is a formatter that will update objects and arrays +// to their canonical form, i.e. sorting object fields and array elements. +type Canonicalize struct { + pass.Base +} + +type SortableField struct { + Kind int + Key string + Field ast.ObjectField +} + +func (c *Canonicalize) sortFields(fields ast.ObjectFields) { + + var sortableFields = make([]SortableField, len(fields)) + + // We first construct a sortable representation of the fields using + // the kind as precedence indicator: + // - asserts + // - locals + // - any object field + for index, field := range fields { + var kind = 0 + + switch field.Kind { + case ast.ObjectAssert: + kind = 0 + case ast.ObjectFieldID: + kind = 2 + case ast.ObjectFieldExpr: + kind = 2 + case ast.ObjectFieldStr: + kind = 2 + case ast.ObjectLocal: + kind = 1 + } + + // generate a string representation of each field + u := &unparser{options: Options{StripEverything: true}} + var singleField = make([]ast.ObjectField, 1) + singleField[0] = field + u.unparseFields(singleField, false) + + sortableFields[index] = SortableField{Kind: kind, Key: u.string(), Field: field} + } + + // sort the fields using a stable sort to ensure that order + // of some fields (local and assert expressions) is retained as + // contained in the original ast. + sort.SliceStable(sortableFields, func(i, j int) bool { + if sortableFields[i].Kind != sortableFields[j].Kind { + return sortableFields[i].Kind < sortableFields[j].Kind + } + + // retain original order local and assert expressions, + if sortableFields[i].Kind < 2 { + return false + } else { + return sortableFields[i].Key < sortableFields[j].Key + } + }) + + for index, field := range sortableFields { + fields[index] = field.Field + } +} + +type SortableElement struct { + Key string + Element ast.CommaSeparatedExpr +} + +func (c *Canonicalize) sortArrayElements(elements []ast.CommaSeparatedExpr) { + var sortableElements = make([]SortableElement, len(elements)) + + for index, element := range elements { + u := &unparser{options: Options{StripEverything: true}} + u.unparse(element.Expr, false) + sortableElements[index] = SortableElement{Key: u.string(), Element: element} + } + + sort.SliceStable(sortableElements, func(i, j int) bool { + return sortableElements[i].Key < sortableElements[j].Key + }) + + for index, element := range sortableElements { + elements[index] = element.Element + } +} + +// Array handles that type of node +func (c *Canonicalize) Array(p pass.ASTPass, node *ast.Array, ctx pass.Context) { + if len(node.Elements) == 0 { + // No comma present and none can be added. + return + } + c.sortArrayElements(node.Elements) + c.Base.Array(p, node, ctx) +} + +// Object handles that type of node +func (c *Canonicalize) Object(p pass.ASTPass, node *ast.Object, ctx pass.Context) { + if len(node.Fields) == 0 { + // No fields present. + return + } + c.sortFields(node.Fields) + c.Base.Object(p, node, ctx) +} diff --git a/internal/formatter/jsonnetfmt.go b/internal/formatter/jsonnetfmt.go index 574d5207..84446afd 100644 --- a/internal/formatter/jsonnetfmt.go +++ b/internal/formatter/jsonnetfmt.go @@ -74,6 +74,8 @@ type Options struct { StripEverything bool StripComments bool StripAllButComments bool + + Canonicalize bool } // DefaultOptions returns the recommended formatter behaviour. @@ -88,6 +90,7 @@ func DefaultOptions() Options { PadArrays: false, PadObjects: true, SortImports: true, + Canonicalize: false, } } @@ -195,6 +198,10 @@ func FormatNode(node ast.Node, finalFodder ast.Fodder, options Options) (string, visitor := FixIndentation{Options: options} visitor.VisitFile(node, finalFodder) } + if options.Canonicalize { + visitFile(&Canonicalize{}, &node, &finalFodder) + } + removeExtraTrailingNewlines(finalFodder) u := &unparser{options: options} diff --git a/internal/formatter/unparser.go b/internal/formatter/unparser.go index d3e92cdf..b0469a19 100644 --- a/internal/formatter/unparser.go +++ b/internal/formatter/unparser.go @@ -51,6 +51,9 @@ func (u *unparser) write(str string) { // If crowded is false and separateToken is false then no space is printed // after or before the fodder, even if the last fodder was an interstitial. func (u *unparser) fodderFill(fodder ast.Fodder, crowded bool, separateToken bool, final bool) { + if u.options.StripEverything { + return + } var lastIndent int for i, fod := range fodder { skipTrailing := final && (i == (len(fodder) - 1)) From b9a193225a61151b5fa4eea4589e653867d881f7 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Sat, 12 Aug 2023 08:26:23 +0200 Subject: [PATCH 2/2] Ensure that locals are before assert nodes as assert nodes might reference locals. --- internal/formatter/canonicalize.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/formatter/canonicalize.go b/internal/formatter/canonicalize.go index fc072f38..7f2f4249 100644 --- a/internal/formatter/canonicalize.go +++ b/internal/formatter/canonicalize.go @@ -32,7 +32,7 @@ func (c *Canonicalize) sortFields(fields ast.ObjectFields) { switch field.Kind { case ast.ObjectAssert: - kind = 0 + kind = 1 case ast.ObjectFieldID: kind = 2 case ast.ObjectFieldExpr: @@ -40,7 +40,7 @@ func (c *Canonicalize) sortFields(fields ast.ObjectFields) { case ast.ObjectFieldStr: kind = 2 case ast.ObjectLocal: - kind = 1 + kind = 0 } // generate a string representation of each field