Skip to content

Commit efc00a6

Browse files
authored
feat(sort-keys, sort-array-values): improve to calculate the minimum edit distance for sorting and report the optimal sorting direction (#426)
1 parent da27c0e commit efc00a6

File tree

11 files changed

+759
-240
lines changed

11 files changed

+759
-240
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-jsonc": minor
3+
---
4+
5+
feat(sort-array-values): improve to calculate the minimum edit distance for sorting and report the optimal sorting direction

.changeset/fuzzy-glasses-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-jsonc": minor
3+
---
4+
5+
feat(sort-keys): improve to calculate the minimum edit distance for sorting and report the optimal sorting direction

.eslintrc.for-vscode.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import eslintRuleTester from "eslint-plugin-eslint-rule-tester";
44
export default [
55
...base,
66
{
7-
files: ["tests/src/rules/**/*.ts"],
7+
files: ["tests/lib/rules/**/*.ts"],
88
plugins: { "eslint-rule-tester": eslintRuleTester },
99
rules: {
1010
"eslint-rule-tester/valid-testcase": "error",

lib/rules/sort-array-values.ts

Lines changed: 157 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { AST } from "jsonc-eslint-parser";
44
import { getStaticJSONValue } from "jsonc-eslint-parser";
55
import type { SourceCode } from "eslint";
66
import type { AroundTarget } from "../utils/fix-sort-elements";
7-
import { fixForSorting } from "../utils/fix-sort-elements";
7+
import {
8+
fixToMoveDownForSorting,
9+
fixToMoveUpForSorting,
10+
} from "../utils/fix-sort-elements";
11+
import { calcShortestEditScript } from "../utils/calc-shortest-edit-script";
812

913
type JSONValue = ReturnType<typeof getStaticJSONValue>;
1014

@@ -372,8 +376,10 @@ export default createRule<UserOptions>("sort-array-values", {
372376
},
373377

374378
messages: {
375-
sortValues:
376-
"Expected array values to be in {{orderText}} order. '{{thisValue}}' should be before '{{prevValue}}'.",
379+
shouldBeBefore:
380+
"Expected array values to be in {{orderText}} order. '{{thisValue}}' should be before '{{targetValue}}'.",
381+
shouldBeAfter:
382+
"Expected array values to be in {{orderText}} order. '{{thisValue}}' should be after '{{targetValue}}'.",
377383
},
378384
type: "suggestion",
379385
},
@@ -386,48 +392,154 @@ export default createRule<UserOptions>("sort-array-values", {
386392
const parsedOptions = parseOptions(context.options);
387393

388394
/**
389-
* Verify for array element
395+
* Sort elements by bubble sort.
390396
*/
391-
function verifyArrayElement(data: JSONElementData, option: ParsedOption) {
392-
if (option.ignore(data)) {
393-
return;
397+
function bubbleSort(elements: JSONElementData[], option: ParsedOption) {
398+
const l = elements.length;
399+
const result = [...elements];
400+
let swapped: boolean;
401+
do {
402+
swapped = false;
403+
for (let nextIndex = 1; nextIndex < l; nextIndex++) {
404+
const prevIndex = nextIndex - 1;
405+
if (option.isValidOrder(result[prevIndex], result[nextIndex]))
406+
continue;
407+
[result[prevIndex], result[nextIndex]] = [
408+
result[nextIndex],
409+
result[prevIndex],
410+
];
411+
swapped = true;
412+
}
413+
} while (swapped);
414+
return result;
415+
}
416+
417+
/**
418+
* Verify for array elements
419+
*/
420+
function verifyArrayElements(
421+
elements: JSONElementData[],
422+
option: ParsedOption,
423+
) {
424+
const sorted = bubbleSort(elements, option);
425+
const editScript = calcShortestEditScript(elements, sorted);
426+
for (let index = 0; index < editScript.length; index++) {
427+
const edit = editScript[index];
428+
if (edit.type !== "delete") continue;
429+
const insertEditIndex = editScript.findIndex(
430+
(e) => e.type === "insert" && e.b === edit.a,
431+
);
432+
if (insertEditIndex === -1) {
433+
// should not happen
434+
continue;
435+
}
436+
if (index < insertEditIndex) {
437+
const target = findInsertAfterTarget(edit.a, insertEditIndex);
438+
if (!target) {
439+
// should not happen
440+
continue;
441+
}
442+
context.report({
443+
loc: edit.a.reportLoc,
444+
messageId: "shouldBeAfter",
445+
data: {
446+
thisValue: toText(edit.a),
447+
targetValue: toText(target),
448+
orderText: option.orderText(edit.a),
449+
},
450+
*fix(fixer) {
451+
yield* fixToMoveDownForSorting(
452+
fixer,
453+
sourceCode,
454+
edit.a.around,
455+
target.around,
456+
);
457+
},
458+
});
459+
} else {
460+
const target = findInsertBeforeTarget(edit.a, insertEditIndex);
461+
if (!target) {
462+
// should not happen
463+
continue;
464+
}
465+
context.report({
466+
loc: edit.a.reportLoc,
467+
messageId: "shouldBeBefore",
468+
data: {
469+
thisValue: toText(edit.a),
470+
targetValue: toText(target),
471+
orderText: option.orderText(edit.a),
472+
},
473+
*fix(fixer) {
474+
yield* fixToMoveUpForSorting(
475+
fixer,
476+
sourceCode,
477+
edit.a.around,
478+
target.around,
479+
);
480+
},
481+
});
482+
}
394483
}
395-
const prevList = data.array.elements
396-
.slice(0, data.index)
397-
.reverse()
398-
.filter((d) => !option.ignore(d));
399484

400-
if (prevList.length === 0) {
401-
return;
485+
/**
486+
* Find insert after target
487+
*/
488+
function findInsertAfterTarget(
489+
element: JSONElementData,
490+
insertEditIndex: number,
491+
) {
492+
for (let index = insertEditIndex - 1; index >= 0; index--) {
493+
const edit = editScript[index];
494+
if (edit.type === "delete" && edit.a === element) break;
495+
if (edit.type !== "common") continue;
496+
return edit.a;
497+
}
498+
499+
let lastTarget: JSONElementData | null = null;
500+
for (
501+
let index = elements.indexOf(element) + 1;
502+
index < elements.length;
503+
index++
504+
) {
505+
const el = elements[index];
506+
if (option.isValidOrder(el, element)) {
507+
lastTarget = el;
508+
continue;
509+
}
510+
return lastTarget;
511+
}
512+
return lastTarget;
402513
}
403-
const prev = prevList[0];
404-
if (!option.isValidOrder(prev, data)) {
405-
const reportLoc = data.reportLoc;
406-
context.report({
407-
loc: reportLoc,
408-
messageId: "sortValues",
409-
data: {
410-
thisValue: toText(data),
411-
prevValue: toText(prev),
412-
orderText: option.orderText(data),
413-
},
414-
fix(fixer) {
415-
let moveTarget = prevList[0];
416-
for (const prev of prevList) {
417-
if (option.isValidOrder(prev, data)) {
418-
break;
419-
} else {
420-
moveTarget = prev;
421-
}
422-
}
423-
return fixForSorting(
424-
fixer,
425-
sourceCode,
426-
data.around,
427-
moveTarget.around,
428-
);
429-
},
430-
});
514+
515+
/**
516+
* Find insert before target
517+
*/
518+
function findInsertBeforeTarget(
519+
element: JSONElementData,
520+
insertEditIndex: number,
521+
) {
522+
for (
523+
let index = insertEditIndex + 1;
524+
index < editScript.length;
525+
index++
526+
) {
527+
const edit = editScript[index];
528+
if (edit.type === "delete" && edit.a === element) break;
529+
if (edit.type !== "common") continue;
530+
return edit.a;
531+
}
532+
533+
let lastTarget: JSONElementData | null = null;
534+
for (let index = elements.indexOf(element) - 1; index >= 0; index--) {
535+
const el = elements[index];
536+
if (option.isValidOrder(element, el)) {
537+
lastTarget = el;
538+
continue;
539+
}
540+
return lastTarget;
541+
}
542+
return lastTarget;
431543
}
432544
}
433545

@@ -448,9 +560,10 @@ export default createRule<UserOptions>("sort-array-values", {
448560
if (!option) {
449561
return;
450562
}
451-
for (const element of data.elements) {
452-
verifyArrayElement(element, option);
453-
}
563+
verifyArrayElements(
564+
data.elements.filter((d) => !option.ignore(d)),
565+
option,
566+
);
454567
},
455568
};
456569
},

0 commit comments

Comments
 (0)