fix: correct non-transitive keywordComparator in validation.js#123
Conversation
|
Hi @jdesrosiers , I noticed a bug in the keyword sorting logic where the custom comparator for This can cause unstable or incorrect keyword ordering depending on the JavaScript engine. I've submitted a quick PR to fix it using a standard transitive comparator. All linting and tests pass successfully. Let me know what you think! |
jdesrosiers
left a comment
There was a problem hiding this comment.
There's isn't a bug here or even a potential bug. This comparator will always sort the desired keywords at the end. There's a possibility that other keywords end up in a different order depending on the sort algorithm, but that doesn't matter in this context. As long as the unevaluated keywords end up at the end, it's fine.
I was trying to make this sort as efficient as possible by making the comparator do as little work as possible, but after some thought and research, I've concluded that it's plausible that the sort algorithm has to do more work given this comparator. I'm not 100% convinced, but considering the effort it would take to be sure and that any difference is negligible at these scales, we might as well do it correctly.
| const keywordComparator = (a, b) => { | ||
| const isA = lastKeywords.has(a[0]); | ||
| const isB = lastKeywords.has(b[0]); | ||
| return isA === isB ? 0 : isA ? 1 : -1; | ||
| }; |
There was a problem hiding this comment.
I'm not a big fan of the nested ternary expression. How about this instead.
| const keywordComparator = (a, b) => { | |
| const isA = lastKeywords.has(a[0]); | |
| const isB = lastKeywords.has(b[0]); | |
| return isA === isB ? 0 : isA ? 1 : -1; | |
| }; | |
| const keywordComparator = (a, b) => lastKeywords.has(a[0]) - lastKeywords.has(b[0]); |
There was a problem hiding this comment.
Agreed, that's much cleaner! Applied your suggestion. The boolean-to-number coercion makes it concise and easy to follow once you know the trick.
That's a fair correction, I appreciate you clarifying the intent behind the original implementation. I may have overstated it as a "bug" when it was more of a correctness concern with the sort contract. Thanks for taking the time to think it through and for being open to the change regardless! |
The previous comparator always returned -1 or 1, never 0. This violates the mathematical contract that Array.prototype.sort() requires: compare(a, a) must return 0 (reflexivity) When two non-last-keywords are compared against each other, the old comparator returned 1 (meaning 'a > b'), which is incorrect—they are equal in priority. V8's Timsort can produce non-deterministic or incorrect orderings when comparators break this contract. Replace it with a correct, stable comparator that returns 0 for two elements that have the same 'lastness', 1 if only a is a last-keyword, and -1 if only b is a last-keyword.
422ad0c to
22f888c
Compare
Problem
In
lib/keywords/validation.js, thekeywordComparatoris used to sort compiled keywords so thatunevaluatedPropertiesandunevaluatedItemsare evaluated last.The previous comparator was defined as:
This comparator violates the
Array.prototype.sort()contract:0(which signifies equal sorting priority).AandBresults inA > BandB > Asimultaneously).Because of this, the sorting behavior becomes non-deterministic and engine-dependent. Depending on the JS engine (V8, SpiderMonkey, JSC) or array size,
unevaluatedProperties/unevaluatedItemscould fail to sort to the end, causing unexpected validation bugs.Solution
Refactored the comparator to correctly compare both inputs and respect the sort contract:
This ensures that: