-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cmd/shfmt: implement --from-json and improve -tojson
For the sake of consistency, -tojson is now also available as --to-json. The following changes are made to --to-json: 1) JSON object keys are no longer sorted alphabetically. The new order is: the derived keys (Type, Pos, End), and then the Node's struct fields in their original order. This helps consistency across nodes and with the Go documentation. 2) File.Name is empty by default, rather than `<standard input>`. It did not make sense as a default for emitting JSON, as the flag always required the input to be stdin. 3) Empty values are no longer emitted, to help with verbosity. This includes `false`, `""`, `null`, `[]`, and zero positions. Positions offsets are exempt, as 0 is a valid byte offset. 4) All position fields in Node structs are now emitted. Some positions are redundant given the derived Pos and End keys, but some others are entirely separate, like IfClause.ThenPos. As part of point 1 above, JSON encoding no longer uses Go maps. It now uses reflect.StructOf, which further leans into Go reflection. Any of these four changes could potentially break users, as they slightly change the way we encode syntax trees as JSON. We are working under the assumption that the changes are reasonable, and that any breakage is unlikely and easy to fix. If those assumptions turn out to be false once this change is released, we can always swap the -tojson flag to be exactly the old behavior, and --to-json can then add the new behavior in a safer way. We also had other ideas to further improve the JSON format, such as renaming the Type and Pos/End JSON keys, but we leave those as v4 TODOs as they will surely break most users. The new --from-json flag does the reverse; it decodes the typed JSON, and fills a *syntax.File with all the information. The derived Type field is used to select syntax.Node types, and the derived Pos and End fields are ignored entirely. It's worth noting that neither --to-json nor --from-json are optimized. The decoding side first decodes into an empty interface, for example, which leaves plenty of room for improvement. Once we're happy with the functionality, we can look at faster implementations and even dropping the need for reflection. While here, I noticed that the godoc for Pos.IsValid was slightly wrong. As a proof of concept, the following commands all produce the same result: shfmt <input.sh shfmt --to-json <input.sh | shfmt --from-json shfmt --to-json <input.sh | jq | shfmt --from-json Fixes mvdan#35 again, as we never implemented the "read JSON" side.
- Loading branch information
Showing
9 changed files
with
4,391 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Copyright (c) 2017, Daniel Martí <[email protected]> | ||
// See LICENSE for licensing information | ||
|
||
package main | ||
|
||
import ( | ||
"bytes" | ||
"os" | ||
"strings" | ||
"testing" | ||
|
||
qt "github.com/frankban/quicktest" | ||
|
||
"mvdan.cc/sh/v3/syntax" | ||
) | ||
|
||
func TestRoundtripJSON(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Read testdata files. | ||
inputShell, err := os.ReadFile("testdata/json.sh") | ||
qt.Assert(t, err, qt.IsNil) | ||
inputJSON, err := os.ReadFile("testdata/json.json") | ||
if !*update { // allow it to not exist | ||
qt.Assert(t, err, qt.IsNil) | ||
} | ||
sb := new(strings.Builder) | ||
|
||
// Parse the shell source and check that it is well formatted. | ||
parser := syntax.NewParser(syntax.KeepComments(true)) | ||
node, err := parser.Parse(bytes.NewReader(inputShell), "") | ||
qt.Assert(t, err, qt.IsNil) | ||
|
||
printer := syntax.NewPrinter() | ||
sb.Reset() | ||
err = printer.Print(sb, node) | ||
qt.Assert(t, err, qt.IsNil) | ||
qt.Assert(t, sb.String(), qt.Equals, string(inputShell)) | ||
|
||
// Validate writing the pretty JSON. | ||
sb.Reset() | ||
err = writeJSON(sb, node, true) | ||
qt.Assert(t, err, qt.IsNil) | ||
got := sb.String() | ||
if *update { | ||
err := os.WriteFile("testdata/json.json", []byte(got), 0o666) | ||
qt.Assert(t, err, qt.IsNil) | ||
} else { | ||
qt.Assert(t, got, qt.Equals, string(inputJSON)) | ||
} | ||
|
||
// Ensure we don't use the originally parsed node again. | ||
node = nil | ||
|
||
// Validate reading the pretty JSON and check that it formats the same. | ||
node2, err := readJSON(bytes.NewReader(inputJSON)) | ||
qt.Assert(t, err, qt.IsNil) | ||
|
||
sb.Reset() | ||
err = printer.Print(sb, node2) | ||
qt.Assert(t, err, qt.IsNil) | ||
qt.Assert(t, sb.String(), qt.Equals, string(inputShell)) | ||
|
||
// Validate that emitting the JSON again produces the same result. | ||
sb.Reset() | ||
err = writeJSON(sb, node2, true) | ||
qt.Assert(t, err, qt.IsNil) | ||
got = sb.String() | ||
qt.Assert(t, got, qt.Equals, string(inputJSON)) | ||
} |
Oops, something went wrong.