diff --git a/src/rules/sortObjectLiteralKeysRule.ts b/src/rules/sortObjectLiteralKeysRule.ts new file mode 100644 index 00000000000..2e554da56e7 --- /dev/null +++ b/src/rules/sortObjectLiteralKeysRule.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = "unsorted key '"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new SortedKeyWalker(sourceFile, this.getOptions())); + } +} + +class SortedKeyWalker extends Lint.RuleWalker { + // Stacks are used to maintain state while + // recursing through nested object literals. + private lastSortedKeyStack: string[] = []; + private sortedStateStack: boolean[] = []; + + public visitObjectLiteralExpression(node: ts.ObjectLiteralExpression): void { + this.lastSortedKeyStack.push(""); // Char code 0; every string should be >= to this + this.sortedStateStack.push(true); // Sorted state is always initially true + super.visitObjectLiteralExpression(node); + this.lastSortedKeyStack.pop(); + this.sortedStateStack.pop(); + } + + public visitPropertyAssignment(node: ts.PropertyAssignment): void { + const sortedState = this.sortedStateStack[this.sortedStateStack.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. + if (sortedState) { + const lastSortedKey = this.lastSortedKeyStack[this.lastSortedKeyStack.length - 1]; + const keyNode = node.name; + if (keyNode.kind === ts.SyntaxKind.Identifier) { + const key = ( keyNode).text; + if (key < lastSortedKey) { + const failureString = Rule.FAILURE_STRING + key + "'"; + this.addFailure(this.createFailure(keyNode.getStart(), keyNode.getWidth(), failureString)); + this.sortedStateStack[this.sortedStateStack.length - 1] = false; + } else { + this.lastSortedKeyStack[this.lastSortedKeyStack.length - 1] = key; + } + } + } + super.visitPropertyAssignment(node); + } +} diff --git a/test/files/rules/sortedkey.test.ts b/test/files/rules/sortedkey.test.ts new file mode 100644 index 00000000000..6e4e792647b --- /dev/null +++ b/test/files/rules/sortedkey.test.ts @@ -0,0 +1,79 @@ +var passA = { + a: 1, + b: 2 +}; + +var failA = { + b: 1, + a: 2 +}; + +var passB = { + a: 1, + b: 2, + c: 3, + d: 4 +}; + +var failB = { + c: 3, + a: 1, + b: 2, + d: 4 +}; + +var passC = { + a: 1, + b: { + aa: 1, + bb: 2 + } +}; + +var failC = { + a: 1, + b: { + bb: 2, + aa: 1 + } +}; + +var passD = { + a: 1, + b: { + aa: 1, + bb: 2 + }, + c: 3 +}; + +var failD = { + a: 1, + c: { + aa: 1, + bb: 2 + }, + b: 3 +}; + +var passE = {}; + +var passF = { + asdf: [1, 2, 3], + sdfa: {} +}; + +var failF = { + sdfa: {}, + asdf: [1, 2, 3] +}; + +var passG = { + asdfn: function () {}, + sdafn: function () {} +}; + +var failG = { + sdafn: function () {}, + asdfn: function () {} +}; diff --git a/test/rules/sortObjectLiteralKeysRuleTests.ts b/test/rules/sortObjectLiteralKeysRuleTests.ts new file mode 100644 index 00000000000..7febffe6ade --- /dev/null +++ b/test/rules/sortObjectLiteralKeysRuleTests.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +describe("", () => { + it("forbids unsorted keys in object literals", () => { + const fileName = "rules/sortedkey.test.ts"; + const SortedKeyRule = Lint.Test.getRule("sort-object-literal-keys"); + const failureString = SortedKeyRule.FAILURE_STRING; + + const actualFailures = Lint.Test.applyRuleOnFile(fileName, SortedKeyRule); + const createFailure1 = Lint.Test.createFailuresOnFile(fileName, failureString + "a'"); + const createFailure2 = Lint.Test.createFailuresOnFile(fileName, failureString + "a'"); + const createFailure3 = Lint.Test.createFailuresOnFile(fileName, failureString + "aa'"); + const createFailure4 = Lint.Test.createFailuresOnFile(fileName, failureString + "b'"); + const createFailure5 = Lint.Test.createFailuresOnFile(fileName, failureString + "asdf'"); + const createFailure6 = Lint.Test.createFailuresOnFile(fileName, failureString + "asdfn'"); + const expectedFailures = [ + createFailure1([8, 5], [8, 6]), + createFailure2([20, 5], [20, 6]), + createFailure3([37, 9], [37, 11]), + createFailure4([56, 5], [56, 6]), + createFailure5([68, 5], [68, 9]), + createFailure6([78, 5], [78, 10]) + ]; + + Lint.Test.assertFailuresEqual(actualFailures, expectedFailures); + }); +});