Skip to content

fix: correct non-transitive keywordComparator in validation.js#123

Open
Suyog241005 wants to merge 1 commit into
hyperjump-io:mainfrom
Suyog241005:fix/keyword-sort-comparator
Open

fix: correct non-transitive keywordComparator in validation.js#123
Suyog241005 wants to merge 1 commit into
hyperjump-io:mainfrom
Suyog241005:fix/keyword-sort-comparator

Conversation

@Suyog241005
Copy link
Copy Markdown
Contributor

Problem

In lib/keywords/validation.js, the keywordComparator is used to sort compiled keywords so that unevaluatedProperties and unevaluatedItems are evaluated last.

The previous comparator was defined as:

const keywordComparator = (_a, b) => lastKeywords.has(b[0]) ? -1 : 1;

This comparator violates the Array.prototype.sort() contract:

  1. It never returns 0 (which signifies equal sorting priority).
  2. It is non-transitive and contradictory (e.g., comparing two normal keywords A and B results in A > B and B > A simultaneously).

Because of this, the sorting behavior becomes non-deterministic and engine-dependent. Depending on the JS engine (V8, SpiderMonkey, JSC) or array size, unevaluatedProperties/unevaluatedItems could fail to sort to the end, causing unexpected validation bugs.

Solution

Refactored the comparator to correctly compare both inputs and respect the sort contract:

const keywordComparator = (a, b) => {
  const isA = lastKeywords.has(a[0]);
  const isB = lastKeywords.has(b[0]);
  return isA === isB ? 0 : isA ? 1 : -1;
};

This ensures that:

  • Two keywords of the same priority return 0 (stable sorting).
  • A keyword that belongs at the end returns 1 when compared to a normal one.
  • A keyword that belongs at the end returns -1 when compared from the other side.

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.
@Suyog241005
Copy link
Copy Markdown
Contributor Author

Suyog241005 commented Jun 5, 2026

Hi @jdesrosiers ,

I noticed a bug in the keyword sorting logic where the custom comparator for unevaluatedProperties and unevaluatedItems violates the standard sorting contract (it never returns 0 and has contradictory results when comparing items of equal priority).

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant