Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit 658cc8f

Browse files
andy-hansonadidahiya
authored andcommitted
object-literal-sort-keys: Add "match-declaration-order" option (#2829)
1 parent 3ae9eee commit 658cc8f

File tree

4 files changed

+221
-34
lines changed

4 files changed

+221
-34
lines changed

src/rules/objectLiteralSortKeysRule.ts

Lines changed: 143 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,74 +15,183 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { isObjectLiteralExpression, isSameLine } from "tsutils";
18+
import { isInterfaceDeclaration, isObjectLiteralExpression, isSameLine, isTypeAliasDeclaration, isTypeLiteralNode } from "tsutils";
1919
import * as ts from "typescript";
2020

2121
import * as Lint from "../index";
2222

2323
const OPTION_IGNORE_CASE = "ignore-case";
24+
const OPTION_MATCH_DECLARATION_ORDER = "match-declaration-order";
2425

2526
interface Options {
2627
ignoreCase: boolean;
28+
matchDeclarationOrder: boolean;
2729
}
2830

29-
export class Rule extends Lint.Rules.AbstractRule {
31+
export class Rule extends Lint.Rules.OptionallyTypedRule {
3032
/* tslint:disable:object-literal-sort-keys */
3133
public static metadata: Lint.IRuleMetadata = {
3234
ruleName: "object-literal-sort-keys",
33-
description: "Requires keys in object literals to be sorted alphabetically",
35+
description: "Checks ordering of keys in object literals.",
3436
rationale: "Useful in preventing merge conflicts",
35-
optionsDescription: `You may optionally pass "${OPTION_IGNORE_CASE}" to compare keys case insensitive.`,
37+
optionsDescription: Lint.Utils.dedent`
38+
By default, this rule checks that keys are in alphabetical order.
39+
The following may optionally be passed:
40+
41+
* "${OPTION_IGNORE_CASE}" will to compare keys in a case insensitive way.
42+
* "${OPTION_MATCH_DECLARATION_ORDER} will prefer to use the key ordering of the contextual type of the object literal, as in:
43+
44+
interface I { foo: number; bar: number; }
45+
const obj: I = { foo: 1, bar: 2 };
46+
47+
If a contextual type is not found, alphabetical ordering will be used instead.
48+
`,
3649
options: {
3750
type: "string",
38-
enum: [OPTION_IGNORE_CASE],
51+
enum: [OPTION_IGNORE_CASE, OPTION_MATCH_DECLARATION_ORDER],
3952
},
4053
optionExamples: [
4154
true,
42-
[true, OPTION_IGNORE_CASE],
55+
[true, OPTION_IGNORE_CASE, OPTION_MATCH_DECLARATION_ORDER],
4356
],
4457
type: "maintainability",
4558
typescriptOnly: false,
4659
};
4760
/* tslint:enable:object-literal-sort-keys */
4861

49-
public static FAILURE_STRING_FACTORY(name: string) {
62+
public static FAILURE_STRING_ALPHABETICAL(name: string): string {
5063
return `The key '${name}' is not sorted alphabetically`;
5164
}
65+
public static FAILURE_STRING_USE_DECLARATION_ORDER(propName: string, typeName: string | undefined): string {
66+
const type = typeName === undefined ? "its type declaration" : `'${typeName}'`;
67+
return `The key '${propName}' is not in the same order as it is in ${type}.`;
68+
}
5269

5370
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
54-
return this.applyWithFunction(sourceFile, walk, {
55-
ignoreCase: this.ruleArguments.indexOf(OPTION_IGNORE_CASE) !== -1,
56-
});
71+
const options = parseOptions(this.ruleArguments);
72+
if (options.matchDeclarationOrder) {
73+
throw new Error(`${this.ruleName} needs type info to use "${OPTION_MATCH_DECLARATION_ORDER}".`);
74+
}
75+
return this.applyWithFunction(sourceFile, walk, options);
76+
}
77+
78+
public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
79+
return this.applyWithFunction<Options, Options>(
80+
sourceFile, (ctx) => walk(ctx, program.getTypeChecker()), parseOptions(this.ruleArguments));
5781
}
5882
}
5983

60-
function walk(ctx: Lint.WalkContext<Options>) {
61-
return ts.forEachChild(ctx.sourceFile, function cb(node): void {
62-
if (isObjectLiteralExpression(node) && node.properties.length > 1 &&
63-
!isSameLine(ctx.sourceFile, node.properties.pos, node.end)) {
64-
let lastKey: string | undefined;
65-
const {options: {ignoreCase}} = ctx;
66-
outer: for (const property of node.properties) {
67-
switch (property.kind) {
68-
case ts.SyntaxKind.SpreadAssignment:
69-
lastKey = undefined; // reset at spread
70-
break;
71-
case ts.SyntaxKind.ShorthandPropertyAssignment:
72-
case ts.SyntaxKind.PropertyAssignment:
73-
if (property.name.kind === ts.SyntaxKind.Identifier ||
74-
property.name.kind === ts.SyntaxKind.StringLiteral) {
75-
const key = ignoreCase ? property.name.text.toLowerCase() : property.name.text;
76-
// comparison with undefined is expected
77-
if (lastKey! > key) {
78-
ctx.addFailureAtNode(property.name, Rule.FAILURE_STRING_FACTORY(property.name.text));
79-
break outer; // only show warning on first out-of-order property
80-
}
81-
lastKey = key;
84+
function parseOptions(ruleArguments: any[]): Options {
85+
return {
86+
ignoreCase: has(OPTION_IGNORE_CASE),
87+
matchDeclarationOrder: has(OPTION_MATCH_DECLARATION_ORDER),
88+
};
89+
90+
function has(name: string) {
91+
return ruleArguments.indexOf(name) !== -1;
92+
}
93+
}
94+
95+
function walk(ctx: Lint.WalkContext<Options>, checker?: ts.TypeChecker): void {
96+
const { sourceFile, options: { ignoreCase, matchDeclarationOrder } } = ctx;
97+
ts.forEachChild(sourceFile, function cb(node): void {
98+
if (isObjectLiteralExpression(node) && node.properties.length > 1) {
99+
check(node);
100+
}
101+
ts.forEachChild(node, cb);
102+
});
103+
104+
function check(node: ts.ObjectLiteralExpression): void {
105+
if (matchDeclarationOrder) {
106+
const type = getContextualType(node, checker!);
107+
// If type has an index signature, we can't check ordering.
108+
// If type has call/construct signatures, it can't be satisfied by an object literal anyway.
109+
if (type !== undefined
110+
&& type.members.every((m) => m.kind === ts.SyntaxKind.PropertySignature || m.kind === ts.SyntaxKind.MethodSignature)) {
111+
checkMatchesDeclarationOrder(node, type, type.members as ReadonlyArray<ts.PropertySignature | ts.MethodSignature>);
112+
return;
113+
}
114+
}
115+
checkAlphabetical(node);
116+
}
117+
118+
function checkAlphabetical(node: ts.ObjectLiteralExpression): void {
119+
if (isSameLine(ctx.sourceFile, node.properties.pos, node.end)) {
120+
return;
121+
}
122+
123+
let lastKey: string | undefined;
124+
for (const property of node.properties) {
125+
switch (property.kind) {
126+
case ts.SyntaxKind.SpreadAssignment:
127+
lastKey = undefined; // reset at spread
128+
break;
129+
case ts.SyntaxKind.ShorthandPropertyAssignment:
130+
case ts.SyntaxKind.PropertyAssignment:
131+
if (property.name.kind === ts.SyntaxKind.Identifier ||
132+
property.name.kind === ts.SyntaxKind.StringLiteral) {
133+
const key = ignoreCase ? property.name.text.toLowerCase() : property.name.text;
134+
// comparison with undefined is expected
135+
if (lastKey! > key) {
136+
ctx.addFailureAtNode(property.name, Rule.FAILURE_STRING_ALPHABETICAL(property.name.text));
137+
return; // only show warning on first out-of-order property
82138
}
139+
lastKey = key;
140+
}
141+
}
142+
}
143+
}
144+
145+
function checkMatchesDeclarationOrder(
146+
{ properties }: ts.ObjectLiteralExpression,
147+
type: TypeLike,
148+
members: ReadonlyArray<{ name: ts.PropertyName }>): void {
149+
150+
let memberIndex = 0;
151+
outer: for (const prop of properties) {
152+
if (prop.kind === ts.SyntaxKind.SpreadAssignment) {
153+
memberIndex = 0;
154+
continue;
155+
}
156+
157+
if (prop.name.kind === ts.SyntaxKind.ComputedPropertyName) { continue; }
158+
159+
const propName = prop.name.text;
160+
161+
for (; memberIndex !== members.length; memberIndex++) {
162+
const { name: memberName } = members[memberIndex];
163+
if (memberName.kind !== ts.SyntaxKind.ComputedPropertyName && propName === memberName.text) {
164+
continue outer; // tslint:disable-line no-unsafe-any (fixed in tslint 5.4)
83165
}
84166
}
167+
168+
// This We didn't find the member we were looking for past the previous member,
169+
// so it must have come before it and is therefore out of order.
170+
ctx.addFailureAtNode(prop.name, Rule.FAILURE_STRING_USE_DECLARATION_ORDER(propName, typeName(type)));
171+
// Don't bother with multiple errors.
172+
break;
85173
}
86-
return ts.forEachChild(node, cb);
87-
});
174+
}
175+
}
176+
177+
function typeName(t: TypeLike): string | undefined {
178+
const parent = t.parent!;
179+
return t.kind === ts.SyntaxKind.InterfaceDeclaration ? t.name.text : isTypeAliasDeclaration(parent) ? parent.name.text : undefined;
180+
}
181+
182+
type TypeLike = ts.InterfaceDeclaration | ts.TypeLiteralNode;
183+
184+
function getContextualType(node: ts.Expression, checker: ts.TypeChecker): TypeLike | undefined {
185+
const c = checker.getContextualType(node);
186+
if (c === undefined || c.symbol === undefined) {
187+
return undefined;
188+
}
189+
190+
const { declarations } = c.symbol;
191+
if (declarations === undefined || declarations.length !== 1) {
192+
return undefined;
193+
}
194+
195+
const decl = declarations[0];
196+
return isInterfaceDeclaration(decl) || isTypeLiteralNode(decl) ? decl : undefined;
88197
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
interface I {
2+
b;
3+
a;
4+
}
5+
6+
declare function f(i: I): void;
7+
8+
const a = 0, b = 0;
9+
10+
f({ b, a });
11+
12+
f({ a, b });
13+
~ [0 % ('b', 'I')]
14+
15+
// Resets ordering after spread operator.
16+
17+
f({ a, ...x, b });
18+
19+
f({ a, ...x, a, b });
20+
~ [0 % ('b', 'I')]
21+
22+
23+
// Methods and getters/setters work like any other key.
24+
25+
f({ b() {}, a() {} });
26+
27+
f({ a() {}, b() {} });
28+
~ [0 % ('b', 'I')]
29+
30+
f({
31+
get b() {},
32+
a,
33+
set b(v) {},
34+
~ [0 % ('b', 'I')]
35+
});
36+
37+
f({
38+
get b() {},
39+
set b() {},
40+
a,
41+
});
42+
43+
// Ignores computed properties. Does not ignore string / number keys.
44+
45+
interface J {
46+
"foo";
47+
2;
48+
[Symol.iterator];
49+
}
50+
declare function j(j: J): void;
51+
j({ [Symbol.iterator]: 1, "foo": 1, 2: 1 });
52+
j({ [Symbol.iterator]: 1, 2: 1, "foo": 1 });
53+
~~~~~ [0 % ('foo', 'J')]
54+
55+
// Works with anonymous type too.
56+
type T = { b, a };
57+
const o: T = { a, b };
58+
~ [0 % ('b', 'T')]
59+
60+
const o: { b, a } = { a, b };
61+
~ [1 % ('b')]
62+
63+
// Goes with alphabetical ordering if it can't find a type.
64+
65+
const o = {
66+
b,
67+
a,
68+
~ [The key 'a' is not sorted alphabetically]
69+
};
70+
71+
[0]: The key '%s' is not in the same order as it is in '%s'.
72+
[1]: The key '%s' is not in the same order as it is in its type declaration.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rules": {
3+
"object-literal-sort-keys": [true, "ignore-case", "match-declaration-order"]
4+
}
5+
}

0 commit comments

Comments
 (0)