core/quality/eslint/sort-keys.eslint.rule.ts
core/quality/eslint/sort-keys.eslint.rule.ts is a file in GovLab Quality. 93 lines of code and 18 definitions.
import type { Rule } from "eslint";
import { compareText } from "#core/comparators/text.comparator";
import { govlabMeta } from "#core/factories/eslint.factory";
const MIN_KEYS = 2;
type ObjExpr = Extract<Rule.Node, { type: "ObjectExpression" }>;
type ObjProp = ObjExpr["properties"][number];
const literalKey = function literalKey(value: unknown): string {
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return String(value);
}
return "";
};
const staticKey = function staticKey(prop: ObjProp): string | null {
if (prop.type !== "Property" || prop.computed) {
return null;
}
const { key } = prop;
if (key.type === "Identifier") {
return key.name;
}
if (key.type === "Literal") {
return literalKey(key.value);
}
return null;
};
const isClean = function isClean(context: Rule.RuleContext, obj: ObjExpr): boolean {
for (const prop of obj.properties) {
if (prop.type !== "Property" || prop.computed || prop.kind === "get" || prop.kind === "set") {
return false;
}
}
return context.sourceCode.getCommentsInside(obj).length === 0;
};
const reorderFix = function reorderFix(context: Rule.RuleContext, obj: ObjExpr) {
return (fixer: Rule.RuleFixer): Rule.Fix[] => {
const sorted = [...obj.properties].sort((a, b): number => {
const ka = staticKey(a) ?? "";
const kb = staticKey(b) ?? "";
if (ka < kb) {
return -1;
}
return ka > kb ? 1 : 0;
});
return obj.properties.map((prop, index): Rule.Fix =>
fixer.replaceText(prop, context.sourceCode.getText(sorted[index])),
);
};
};
const reportUnsorted = function reportUnsorted(context: Rule.RuleContext, obj: ObjExpr, clean: boolean): void {
let previous: string | null = null;
let fixAttached = false;
for (const prop of obj.properties) {
const key = staticKey(prop);
if (key === null) {
previous = null;
} else {
if (previous !== null && compareText(key, previous) < 0) {
context.report({
data: { key, prev: previous },
...(clean && !fixAttached ? { fix: reorderFix(context, obj) } : {}),
messageId: "unsorted",
node: prop,
});
fixAttached = true;
}
previous = key;
}
}
};
export default {
create(context: Rule.RuleContext): Rule.RuleListener {
const onObject = (node: Rule.Node): void => {
if (node.type !== "ObjectExpression" || node.properties.length < MIN_KEYS) {
return;
}
reportUnsorted(context, node, isClean(context, node));
};
const handlers: [string, (node: Rule.Node) => void][] = [["ObjectExpression", onObject]];
return Object.fromEntries(handlers);
},
meta: govlabMeta({
canonical: ["sort-order"],
description:
"Require object keys sorted ascending; auto-fixed for comment-free objects without spread/computed/accessor keys",
fixable: "code",
messages: { unsorted: "Object key '{{key}}' should sort before '{{prev}}'." },
ruleId: "sort_keys",
type: "suggestion",
}),
} satisfies Rule.RuleModule;