Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add canonicalize ast pass to jsonnetfmt. #722

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/jsonnetfmt/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:")
Expand Down Expand Up @@ -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" {
Expand Down
117 changes: 117 additions & 0 deletions internal/formatter/canonicalize.go
Original file line number Diff line number Diff line change
@@ -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 = 1
case ast.ObjectFieldID:
kind = 2
case ast.ObjectFieldExpr:
kind = 2
case ast.ObjectFieldStr:
kind = 2
case ast.ObjectLocal:
kind = 0
}

// 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)
}
7 changes: 7 additions & 0 deletions internal/formatter/jsonnetfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type Options struct {
StripEverything bool
StripComments bool
StripAllButComments bool

Canonicalize bool
}

// DefaultOptions returns the recommended formatter behaviour.
Expand All @@ -88,6 +90,7 @@ func DefaultOptions() Options {
PadArrays: false,
PadObjects: true,
SortImports: true,
Canonicalize: false,
}
}

Expand Down Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions internal/formatter/unparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down