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

object-literal-sort-keys: rewrite, handle spread and shorthand #2592

Merged
merged 5 commits into from
May 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 44 additions & 52 deletions src/rules/objectLiteralSortKeysRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,32 @@
* limitations under the License.
*/

import { isObjectLiteralExpression, isSameLine } from "tsutils";
import * as ts from "typescript";

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

const OPTION_IGNORE_CASE = "ignore-case";

interface Options {
ignoreCase: boolean;
}

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "object-literal-sort-keys",
description: "Requires keys in object literals to be sorted alphabetically",
rationale: "Useful in preventing merge conflicts",
optionsDescription: "Not configurable.",
options: null,
optionExamples: [true],
optionsDescription: `You may optionally pass "${OPTION_IGNORE_CASE}" to compare keys case insensitive.`,
options: {
type: "string",
enum: [OPTION_IGNORE_CASE],
},
optionExamples: [
true,
[true, OPTION_IGNORE_CASE],
],
type: "maintainability",
typescriptOnly: false,
};
Expand All @@ -38,59 +51,38 @@ export class Rule extends Lint.Rules.AbstractRule {
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new ObjectLiteralSortKeysWalker(sourceFile, this.getOptions()));
return this.applyWithFunction(sourceFile, walk, {
ignoreCase: this.ruleArguments.indexOf(OPTION_IGNORE_CASE) !== -1,
});
}
}

class ObjectLiteralSortKeysWalker extends Lint.RuleWalker {
// stacks are used to maintain state while recursing through nested object literals
private lastSortedKeyStack: string[] = [];
private multilineFlagStack: boolean[] = [];
private sortedStateStack: boolean[] = [];

public visitObjectLiteralExpression(node: ts.ObjectLiteralExpression) {
// char code 0; every string should be >= to this
this.lastSortedKeyStack.push("");
// sorted state is always initially true
this.sortedStateStack.push(true);
this.multilineFlagStack.push(this.isMultilineListNode(node));
super.visitObjectLiteralExpression(node);
this.multilineFlagStack.pop();
this.lastSortedKeyStack.pop();
this.sortedStateStack.pop();
}

public visitPropertyAssignment(node: ts.PropertyAssignment) {
const sortedState = this.sortedStateStack[this.sortedStateStack.length - 1];
const isMultiline = this.multilineFlagStack[this.multilineFlagStack.length - 1];

// skip remainder of object literal scan if a previous key was found
// in an unsorted position. This ensures only one error is thrown at
// a time and keeps error output clean. Skip also single line objects.
if (sortedState && isMultiline) {
const lastSortedKey = this.lastSortedKeyStack[this.lastSortedKeyStack.length - 1];
const keyNode = node.name;
if (isIdentifierOrStringLiteral(keyNode)) {
const key = keyNode.text;
if (key < lastSortedKey) {
const failureString = Rule.FAILURE_STRING_FACTORY(key);
this.addFailureAtNode(keyNode, failureString);
this.sortedStateStack[this.sortedStateStack.length - 1] = false;
} else {
this.lastSortedKeyStack[this.lastSortedKeyStack.length - 1] = key;
function walk(ctx: Lint.WalkContext<Options>) {
return ts.forEachChild(ctx.sourceFile, function cb(node): void {
if (isObjectLiteralExpression(node) && node.properties.length > 1 &&
!isSameLine(ctx.sourceFile, node.properties.pos, node.end)) {
let lastKey: string | undefined;
const {options: {ignoreCase}} = ctx;
outer: for (const property of node.properties) {
switch (property.kind) {
case ts.SyntaxKind.SpreadAssignment:
lastKey = undefined; // reset at spread
break;
case ts.SyntaxKind.ShorthandPropertyAssignment:
case ts.SyntaxKind.PropertyAssignment:
if (property.name.kind === ts.SyntaxKind.Identifier ||
property.name.kind === ts.SyntaxKind.StringLiteral) {
const key = ignoreCase ? property.name.text.toLowerCase() : property.name.text;
// comparison with undefined is expected
if (lastKey! > key) {
ctx.addFailureAtNode(property.name, Rule.FAILURE_STRING_FACTORY(property.name.text));
break outer; // only show warning on first out-of-order property
}
lastKey = key;
}
}
}
}
super.visitPropertyAssignment(node);
}

private isMultilineListNode(node: ts.ObjectLiteralExpression) {
const startLineOfNode = this.getLineAndCharacterOfPosition(node.getStart()).line;
const endLineOfNode = this.getLineAndCharacterOfPosition(node.getEnd()).line;
return endLineOfNode !== startLineOfNode;
}
}

function isIdentifierOrStringLiteral(node: ts.Node): node is (ts.Identifier | ts.StringLiteral) {
return node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.StringLiteral;
return ts.forEachChild(node, cb);
});
}
2 changes: 1 addition & 1 deletion src/rules/quotemarkRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export class Rule extends Lint.Rules.AbstractRule {
const args = this.ruleArguments;
const quoteMark = args[0] === OPTION_SINGLE ? "'" : '"';
return this.applyWithFunction(sourceFile, walk, {
quoteMark,
avoidEscape: args.indexOf(OPTION_AVOID_ESCAPE) !== -1,
jsxQuoteMark: args.indexOf(OPTION_JSX_SINGLE) !== -1
? "'"
: args.indexOf(OPTION_JSX_DOUBLE) !== -1 ? '"' : quoteMark,
quoteMark,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ var passA = {
var failA = {
b: 1,
a: 2
~ [The key 'a' is not sorted alphabetically]
~ [err % ('a')]
};

var passB = {
Expand All @@ -19,7 +19,7 @@ var passB = {
var failB = {
c: 3,
a: 1,
~ [The key 'a' is not sorted alphabetically]
~ [err % ('a')]
b: 2,
d: 4
};
Expand All @@ -37,7 +37,7 @@ var failC = {
b: {
bb: 2,
aa: 1
~~ [The key 'aa' is not sorted alphabetically]
~~ [err % ('aa')]
}
};

Expand All @@ -57,7 +57,7 @@ var failD = {
bb: 2
},
b: 3
~ [The key 'b' is not sorted alphabetically]
~ [err % ('b')]
};

var passE = {};
Expand All @@ -70,7 +70,7 @@ var passF = {
var failF = {
sdfa: {},
asdf: [1, 2, 3]
~~~~ [The key 'asdf' is not sorted alphabetically]
~~~~ [err % ('asdf')]
};

var passG = {
Expand All @@ -81,7 +81,7 @@ var passG = {
var failG = {
sdafn: function () {},
asdfn: function () {}
~~~~~ [The key 'asdfn' is not sorted alphabetically]
~~~~~ [err % ('asdfn')]
};

var passH = {
Expand All @@ -93,7 +93,7 @@ var passH = {
var failH = {
'b': 2,
a: 1,
~ [The key 'a' is not sorted alphabetically]
~ [err % ('a')]
c: 3
}

Expand All @@ -106,7 +106,7 @@ var passI = {
var failI = {
À: 2,
'Z': 1,
~~~ [The key 'Z' is not sorted alphabetically]
~~~ [err % ('Z')]
è: 3,
}

Expand All @@ -122,7 +122,7 @@ var failJ = {
2: 4,
A: 3,
'11': 2
~~~~ [The key '11' is not sorted alphabetically]
~~~~ [err % ('11')]
}

var passK = {
Expand All @@ -138,10 +138,10 @@ var failK = {
'b': {
e: 5,
'd': 4
~~~ [The key 'd' is not sorted alphabetically]
~~~ [err % ('d')]
},
a: 1,
~ [The key 'a' is not sorted alphabetically]
~ [err % ('a')]
c: 3
}

Expand All @@ -150,7 +150,7 @@ var passL = {z: 1, y: '1', x: [1, 2]};
var failL = {x: 1, y: {
b: 1,
a: 2
~ [The key 'a' is not sorted alphabetically]
~ [err % ('a')]
}, z: [1, 2]};

var passM = {
Expand All @@ -161,3 +161,34 @@ var passM = {
},
z: {z: 1, y: '1', x: [1, 2]}
};

const spread = {
c,
...x,
b: b,
a,
~ [err % ('a')]
};

const numbers = {
1: true,
2: true,
100: true,
1e4: true,
2e-1: true,
}

const a = {
array: [],
objList: [{}, {}],
object: {},
}

const b = {
array: [],
object: {},
objList: [{}, {}],
~~~~~~~ [err % ('objList')]
}

[err]: The key '%s' is not sorted alphabetically
12 changes: 12 additions & 0 deletions test/rules/object-literal-sort-keys/ignore-case/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const a = {
array: [],
objList: [{}, {}],
object: {},
~~~~~~ [The key 'object' is not sorted alphabetically]
}

const b = {
array: [],
object: {},
objList: [{}, {}],
}
5 changes: 5 additions & 0 deletions test/rules/object-literal-sort-keys/ignore-case/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"object-literal-sort-keys": [true, "ignore-case"]
}
}