feat: sort object properties and array elements - #15
Merged
Merged
Conversation
Exposes the CST sorting added in dprint/jsonc-parser#88, which moves what was written with a member along with it: the comments and blank lines above it, and a comment written after it on the same line. Commas are moved to suit the new order. root.asObjectOrThrow().sortProperties(); root.asArrayOrThrow().sortElements(); Both take an optional comparator, shaped like the callback Array.prototype.sort takes. Properties are sorted by name and elements by their text when it is omitted. The comparing runs before anything is moved, so a comparator that throws leaves the container exactly as it was and the error is rethrown, and the order it produced is applied as a key rather than by comparing again, which keeps the sort itself consistent whatever the comparator answered. Two accessors came along because a comparator needs them: decodedName on ObjectProp, the name to sort by with its escapes resolved, and toString on Node, which is what the default element sort compares and was otherwise unreachable. jsonc-parser is patched to a sibling checkout until a release carries the sorting; the patch in Cargo.toml comes back out then.
Follows the jsonc-parser API through: sortProperties and sortElements take an options object alongside the comparator. pinCommentHeaders?: boolean | ((member, comments) => number) withinGroups?: boolean true pins every comment that has a blank line above it, which reads as a heading for the members beneath rather than as a description of the first of them. A function is asked per member and handed the comments written above it, returning how many of them stay where they are, which covers a block that is partly a heading and partly a note about the member beneath. withinGroups sorts each run of members between blank lines on its own, so that no member crosses one. hasBlankLineBefore comes along on ObjectProp and Node, since that is what a pinCommentHeaders function needs to tell the two kinds of comment apart.
A comparator that contradicts itself is ordinary in JavaScript -- writing `(a, b) => a.name > b.name` is a common way to get one -- and the order was worked out with Rust's sort, which answers that by panicking. Through wasm that is a trap with no message that takes the whole module down. Merging by hand instead leaves such a comparator with an order nobody promised anything about, the same as Array.prototype.sort does. The comparator's answer is also read the way Array.prototype.sort reads it, through ToNumber rather than only accepting a number, so the `>` comparator above sorts instead of silently doing nothing. A pin rule is handed an ObjectProp when sorting properties and a Node when sorting elements, so SortOptions takes the member type as a parameter and each sort declares which it passes. Before this, reading `decodedName()` off the member was a type error against a working runtime. ObjectProp gains toString, without which a property comparator cannot see the text of what it is comparing. Passing options where the comparator goes now says so, rather than failing with `arg0.call is not a function`.
Drops the patch that pointed at a sibling checkout while the CST sorting was unreleased, and puts the registry source and checksum back in the lock file.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Exposes the CST sorting from dprint/jsonc-parser#88, released as 0.33.2. Sorting moves what was written with a member along with it — the comments and blank lines above it, and a comment written after it on the same line — which is the thing you can't get by reading the document out and writing it back.
API
Properties sort by name and elements by their text when no comparator is given. The comparator is shaped like the callback
Array.prototype.sorttakes, so a conventional field order reads the way you'd expect:Both sorts are stable, and both keep whichever of a trailing comma or none the container was written with.
Deciding what travels
A comment above a member travels with it, which is right for one describing it and wrong for one heading a group. The options say which is which:
pinCommentHeaders: true— a comment with a blank line above it stays where it was written while members sort past it. The blank line is what tells the two kinds apart, so a comment flush against its member still travels.pinCommentHeaders: fn— handed a member and the comments above it, returns how many of them, counting from the top, stay put. For a block that is partly a heading and partly a note about the member beneath it, which a boolean can't express.withinGroups: true— each run of members between blank lines sorts on its own and no member crosses one. A blank line and whatever is under it is the boundary, and boundaries don't move.pinCommentHeaderspins the comment but members still sort past the blank line;withinGroupsis the one that stops them. The README covers the distinction.Comparator errors, and comparators JS actually writes
The comparing runs to completion before anything moves, and the resulting order is then applied as a key. A comparator that throws therefore leaves the container exactly as it was and the error is rethrown — rather than half sorted, which is what
Array.prototype.sortwould leave you.Two things a first cut got wrong here, both now covered by tests:
(a, b) => a.name > b.nameis a normal way to write one in JS, and Rust's sort answers that by panicking — through wasm,RuntimeError: unreachable, with the whole module down. The order is now merged by hand, so such a comparator gets an unspecified-but-safe order, the same as JS gives.ToNumber. Read the same way now, sotrueis 1.Accessors that came along
Each is something a comparator or pin rule needs and none existed:
ObjectProp.decodedName()— the name to sort or look up by, with escapes resolved.ObjectProp.toString()— without it a property comparator can't see the text of what it's comparing.Node.toString()—RootNodehad this butNodedidn't, so the text the default element sort compares was unreachable from a comparator. My firstsortElementstest silently did nothing becausea.toString()was[object Object].hasBlankLineBefore()on both — apinCommentHeadersfunction can't tell the two kinds of comment apart without it.SortOptionstakes the member type as a parameter because a property sort hands the rule anObjectPropand an element sort hands it aNode; before that, readingdecodedName()off the member was a type error against a working runtime.Passing options where the comparator goes now says so, instead of failing with
arg0.call is not a function.Testing
176 tests pass,
deno fmt,deno lintand wasm32cargo clippyare clean. New cases cover: default and comparator sorts for both containers, comments travelling with their member, stability with duplicate names, trailing-comma style preserved, nested containers untouched, a conventional field order, all threepinCommentHeadersforms,withinGroupson objects and arrays, the two combined, a throwing comparator leaving the container byte-identical, a contradictory comparator, a boolean-returning comparator, and the options-in-the-comparator-slot guard.The three
cargo fmtdiffs inrs_lib/src/lib.rsare pre-existing onmainand untouched.