diff --git a/src/ruleLoader.ts b/src/ruleLoader.ts index 5b47c0728fe..de2634c4849 100644 --- a/src/ruleLoader.ts +++ b/src/ruleLoader.ts @@ -59,6 +59,7 @@ export function loadRules(ruleConfiguration: {[name: string]: any}, Try upgrading TSLint and/or ensuring that you have all necessary custom rules installed. If TSLint was recently upgraded, you may have old rules configured which need to be cleaned up. `; + throw new Error(ERROR_MESSAGE); } else { return rules; diff --git a/src/rules/objectLiteralKeyQuotesRule.ts b/src/rules/objectLiteralKeyQuotesRule.ts new file mode 100644 index 00000000000..75d4452db23 --- /dev/null +++ b/src/rules/objectLiteralKeyQuotesRule.ts @@ -0,0 +1,95 @@ +import * as Lint from "../lint"; +import * as ts from "typescript"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "object-literal-key-quotes", + description: "Enforces consistent object literal property quote style.", + descriptionDetails: Lint.Utils.dedent` + Object literal property names can be defined in two ways: using literals or using strings. + For example, these two objects are equivalent: + + var object1 = { + property: true + }; + + var object2 = { + "property": true + }; + + In many cases, it doesn’t matter if you choose to use an identifier instead of a string + or vice-versa. Even so, you might decide to enforce a consistent style in your code. + + This rules lets you enforce consistent quoting of property names. Either they should always + be quoted (default behavior) or quoted only as needed ("as-needed").`, + optionsDescription: Lint.Utils.dedent` + Possible settings are: + + * \`"always"\`: Property names should always be quoted. (This is the default.) + * \`"as-needed"\`: Only property names which require quotes may be quoted (e.g. those with spaces in them). + + For ES6, computed property names (\`{[name]: value}\`) and methods (\`{foo() {}}\`) never need + to be quoted.`, + options: { + type: "string", + enum: ["always", "as-needed"], + // TODO: eslint also supports "consistent", "consistent-as-needed" modes. + // TODO: eslint supports "keywords", "unnecessary" and "numbers" options. + }, + optionExamples: ["[true, \"as-needed\"]", "[true, \"always\"]"], + type: "style", + }; + /* tslint:enable:object-literal-sort-keys */ + + public static UNNEEDED_QUOTES = (name: string) => `Unnecessarily quoted property '${name}' found.`; + public static UNQUOTED_PROPERTY = (name: string) => `Unquoted property '${name}' found.`; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + const objectLiteralKeyQuotesWalker = new ObjectLiteralKeyQuotesWalker(sourceFile, this.getOptions()); + return this.applyWithWalker(objectLiteralKeyQuotesWalker); + } +} + +// This is simplistic. See https://mothereff.in/js-properties for the gorey details. +const IDENTIFIER_NAME_REGEX = /^(?:[\$A-Z_a-z])*$/; + +const NUMBER_REGEX = /^[0-9]+$/; + +type QuotesMode = "always" | "as-needed"; + +class ObjectLiteralKeyQuotesWalker extends Lint.RuleWalker { + private mode: QuotesMode; + + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + + this.mode = this.getOptions()[0] || "always"; + } + + public visitPropertyAssignment(node: ts.PropertyAssignment) { + const name = node.name; + if (this.mode === "always") { + if (name.kind !== ts.SyntaxKind.StringLiteral && + name.kind !== ts.SyntaxKind.ComputedPropertyName) { + this.addFailure(this.createFailure(name.getStart(), name.getWidth(), + Rule.UNQUOTED_PROPERTY(name.getText()))); + } + } else if (this.mode === "as-needed") { + if (name.kind === ts.SyntaxKind.StringLiteral) { + // Check if the quoting is necessary. + const stringNode = name as ts.StringLiteral; + const property = stringNode.text; + + const isIdentifier = IDENTIFIER_NAME_REGEX.test(property); + const isNumber = NUMBER_REGEX.test(property); + if (isIdentifier || (isNumber && Number(property).toString() === property)) { + this.addFailure(this.createFailure(stringNode.getStart(), stringNode.getWidth(), + Rule.UNNEEDED_QUOTES(property))); + } + } + } + + super.visitPropertyAssignment(node); + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 9d1a4a5a91b..4a260213416 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -119,6 +119,7 @@ "rules/noUseBeforeDeclareRule.ts", "rules/noVarKeywordRule.ts", "rules/noVarRequiresRule.ts", + "rules/objectLiteralKeyQuotesRule.ts", "rules/objectLiteralSortKeysRule.ts", "rules/oneLineRule.ts", "rules/oneVariablePerDeclarationRule.ts", diff --git a/test/rules/object-literal-key-quotes/always/test.ts.lint b/test/rules/object-literal-key-quotes/always/test.ts.lint new file mode 100644 index 00000000000..1f245022bd5 --- /dev/null +++ b/test/rules/object-literal-key-quotes/always/test.ts.lint @@ -0,0 +1,23 @@ +const o = { + 'hello': 123, + goodbye: 234, // failure + ~~~~~~~ [Unquoted property 'goodbye' found.] + "quote": 345, + "needs quote": 789, + "hyphens-need-quotes": null, + [computed]: 456, + 123: "hello", // failure + ~~~ [Unquoted property '123' found.] + 1e4: "something", // failure + ~~~ [Unquoted property '1e4' found.] + .123: "float", // failure + ~~~~ [Unquoted property '.123' found.] + '123': 'numbers do not need quotes', + '010': 'but this one does.', + '.123': 'as does this one', + fn() { return }, + true: 0, // failure + ~~~~ [Unquoted property 'true' found.] + "0x0": 0, + "true": 0, +}; diff --git a/test/rules/object-literal-key-quotes/always/tslint.json b/test/rules/object-literal-key-quotes/always/tslint.json new file mode 100644 index 00000000000..a7927997726 --- /dev/null +++ b/test/rules/object-literal-key-quotes/always/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "object-literal-key-quotes": [true, "always"] + } +} diff --git a/test/rules/object-literal-key-quotes/as-needed/test.ts.lint b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint new file mode 100644 index 00000000000..e6dc627b5d0 --- /dev/null +++ b/test/rules/object-literal-key-quotes/as-needed/test.ts.lint @@ -0,0 +1,22 @@ +const o = { + 'hello': 123, // failure + ~~~~~~~ [Unnecessarily quoted property 'hello' found.] + goodbye: 234, + "quote": 345, // failure + ~~~~~~~ [Unnecessarily quoted property 'quote' found.] + "needs quote": 789, + "hyphens-need-quotes": null, + [computed]: 456, + 123: "hello", + 1e4: "something", + .123: "float", + '123': 'numbers do not need quotes', // failure + ~~~~~ [Unnecessarily quoted property '123' found.] + '010': 'but this one does.', + '.123': 'as does this one', + fn() { return }, + true: 0, + "0x0": 0, + "true": 0, // failure + ~~~~~~ [Unnecessarily quoted property 'true' found.] +}; diff --git a/test/rules/object-literal-key-quotes/as-needed/tslint.json b/test/rules/object-literal-key-quotes/as-needed/tslint.json new file mode 100644 index 00000000000..a366a23efb2 --- /dev/null +++ b/test/rules/object-literal-key-quotes/as-needed/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "object-literal-key-quotes": [true, "as-needed"] + } +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 54784774e94..e7d88fe3a94 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -115,6 +115,7 @@ "../src/rules/noUseBeforeDeclareRule.ts", "../src/rules/noVarKeywordRule.ts", "../src/rules/noVarRequiresRule.ts", + "../src/rules/objectLiteralKeyQuotesRule.ts", "../src/rules/objectLiteralSortKeysRule.ts", "../src/rules/oneLineRule.ts", "../src/rules/oneVariablePerDeclarationRule.ts", @@ -162,4 +163,4 @@ "rule-tester/testData.ts", "rule-tester/utilsTests.ts" ] -} +} \ No newline at end of file