# core/quality/eslint/sort-keys.eslint.rule.ts

> 93 lines of code and 18 definitions.

Tree: GovLab Quality
Language: typescript
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-quality-eslint-sort-keys-eslint-rule-ts
Source text: https://banes-lab.com/source/quality/core/quality/eslint/sort-keys.eslint.rule.ts.txt

Listed in [core/quality/eslint](https://banes-lab.com/api/source/quality/core/quality/eslint.md), after [core/quality/eslint/sort-imports.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/sort-imports.eslint.rule.ts.md) and before [core/quality/eslint/valid-test-surface.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/valid-test-surface.eslint.rule.ts.md).

## Definitions

- `reportUnsorted` (lexical_declaration, line 59)
- `literalKey` (lexical_declaration, line 10)
- `isClean` (lexical_declaration, line 34)
- `reorderFix` (lexical_declaration, line 43)
- `onObject` (lexical_declaration, line 83, exported)
- `staticKey` (lexical_declaration, line 20)
- `MIN_KEYS` (lexical_declaration, line 5)
- `ObjExpr` (type_alias_declaration, line 7)
- `ObjProp` (type_alias_declaration, line 8)
- `{ key }` (lexical_declaration, line 24)
- `sorted` (lexical_declaration, line 45)
- `ka` (lexical_declaration, line 46)
- `kb` (lexical_declaration, line 47)
- `previous` (lexical_declaration, line 60)
- `fixAttached` (lexical_declaration, line 61)
- `key` (lexical_declaration, line 63)
- `create` (method_definition, line 82, exported)
- `handlers` (lexical_declaration, line 89, exported)

## Contained in

- [core/quality/eslint](https://banes-lab.com/anatomy/quality/folder-quality-core-quality-eslint.md)

## Uses

- [core/comparators/text.comparator.ts](https://banes-lab.com/source/quality/core/comparators/text.comparator.ts.md)

## Linked from

- [core/comparators](https://banes-lab.com/anatomy/quality/folder-quality-core-comparators.md)
- [core/quality/eslint](https://banes-lab.com/anatomy/quality/folder-quality-core-quality-eslint.md)

## Source

```typescript
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;
```
