diff --git a/src/configs/all.ts b/src/configs/all.ts index 628fcce31bd..f6a7589202d 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -266,7 +266,7 @@ export const rules = { "switch-final-break": true, "type-literal-delimiter": true, "unnecessary-bind": true, - "variable-name": { options: ["ban-keywords", "check-format"] }, + "variable-name": { options: ["ban-keywords", "check-format", "require-const-for-all-caps"] }, whitespace: { options: [ "check-branch", diff --git a/src/rules/variableNameRule.ts b/src/rules/variableNameRule.ts index 91e3f6f3749..883d11bf910 100644 --- a/src/rules/variableNameRule.ts +++ b/src/rules/variableNameRule.ts @@ -17,7 +17,7 @@ // tslint:disable object-literal-sort-keys -import { hasModifier } from "tsutils"; +import * as tsutils from "tsutils"; import * as ts from "typescript"; import * as Lint from "../index"; @@ -41,6 +41,7 @@ const OPTION_LEADING_UNDERSCORE = "allow-leading-underscore"; const OPTION_TRAILING_UNDERSCORE = "allow-trailing-underscore"; const OPTION_BAN_KEYWORDS = "ban-keywords"; const OPTION_CHECK_FORMAT = "check-format"; +const OPTION_REQUIRE_CONT_FOR_ALL_CAPS = "require-const-for-all-caps"; const OPTION_ALLOW_PASCAL_CASE = "allow-pascal-case"; const OPTION_ALLOW_SNAKE_CASE = "allow-snake-case"; @@ -49,11 +50,12 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: "variable-name", description: "Checks variable names for various errors.", optionsDescription: Lint.Utils.dedent` - Five arguments may be optionally provided: + Six arguments may be optionally provided: * \`"${OPTION_CHECK_FORMAT}"\`: allows only lowerCamelCased or UPPER_CASED variable names * \`"${OPTION_LEADING_UNDERSCORE}"\` allows underscores at the beginning (only has an effect if "check-format" specified) * \`"${OPTION_TRAILING_UNDERSCORE}"\` allows underscores at the end. (only has an effect if "check-format" specified) + * \`"${OPTION_REQUIRE_CONT_FOR_ALL_CAPS}"\`: enforces that all variables with UPPER_CASED names should be \`const\`. * \`"${OPTION_ALLOW_PASCAL_CASE}"\` allows PascalCase in addition to lowerCamelCase. * \`"${OPTION_ALLOW_SNAKE_CASE}"\` allows snake_case in addition to lowerCamelCase. * \`"${OPTION_BAN_KEYWORDS}"\`: disallows the use of certain TypeScript keywords as variable or parameter names. @@ -72,7 +74,7 @@ export class Rule extends Lint.Rules.AbstractRule { ], }, minLength: 0, - maxLength: 5, + maxLength: 6, }, optionExamples: [[true, "ban-keywords", "check-format", "allow-leading-underscore"]], type: "style", @@ -81,6 +83,8 @@ export class Rule extends Lint.Rules.AbstractRule { public static KEYWORD_FAILURE = "variable name clashes with keyword/type"; + public static FAILURE_STRING_CONST = "Only `const` variables may be UPPER_CASE."; + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithFunction(sourceFile, walk, parseOptions(this.ruleArguments)); } @@ -91,6 +95,7 @@ interface Options { checkFormat: boolean; leadingUnderscore: boolean; trailingUnderscore: boolean; + allCapsForConst: boolean; allowPascalCase: boolean; allowSnakeCase: boolean; } @@ -102,6 +107,7 @@ function parseOptions(ruleArguments: string[]): Options { checkFormat: !banKeywords || hasOption(OPTION_CHECK_FORMAT), leadingUnderscore: hasOption(OPTION_LEADING_UNDERSCORE), trailingUnderscore: hasOption(OPTION_TRAILING_UNDERSCORE), + allCapsForConst: hasOption(OPTION_REQUIRE_CONT_FOR_ALL_CAPS), allowPascalCase: hasOption(OPTION_ALLOW_PASCAL_CASE), allowSnakeCase: hasOption(OPTION_ALLOW_SNAKE_CASE), }; @@ -133,31 +139,37 @@ function walk(ctx: Lint.WalkContext): void { case ts.SyntaxKind.VariableStatement: // skip 'declare' keywords - if (hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)) { + if (tsutils.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)) { return; } break; case ts.SyntaxKind.Parameter: case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.VariableDeclaration: { - const { name, initializer } = node as - | ts.ParameterDeclaration - | ts.PropertyDeclaration - | ts.VariableDeclaration; - if (name.kind === ts.SyntaxKind.Identifier) { - handleVariableNameFormat(name, initializer); - // do not check property declarations for keywords, they are allowed to be keywords - if (node.kind !== ts.SyntaxKind.PropertyDeclaration) { - handleVariableNameKeyword(name); - } - } - } + handleDeclaredVariable(node as ts.ParameterDeclaration | ts.PropertyDeclaration); + break; + + case ts.SyntaxKind.VariableDeclaration: + handleVariableDeclaration(node as ts.VariableDeclaration); } return ts.forEachChild(node, cb); }); + function handleDeclaredVariable( + node: ts.ParameterDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration, + ): void { + const { name, initializer } = node; + + if (name.kind === ts.SyntaxKind.Identifier) { + handleVariableNameFormat(name, initializer); + // do not check property declarations for keywords, they are allowed to be keywords + if (node.kind !== ts.SyntaxKind.PropertyDeclaration) { + handleVariableNameKeyword(name); + } + } + } + function handleVariableNameFormat(name: ts.Identifier, initializer?: ts.Expression): void { if (!options.checkFormat) { return; @@ -179,6 +191,21 @@ function walk(ctx: Lint.WalkContext): void { } } + function handleVariableDeclaration(node: ts.VariableDeclaration): void { + handleDeclaredVariable(node); + + if (!ctx.options.allCapsForConst || tsutils.isBindingPattern(node.name)) { + return; + } + + const declarationList = node.parent; + const text = node.name.text; + + if (isUpperCase(text) && !tsutils.isNodeFlagSet(declarationList, ts.NodeFlags.Const)) { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING_CONST); + } + } + function formatFailure(): string { let failureMessage = "variable name must be in lowerCamelCase"; if (options.allowPascalCase) { diff --git a/test/rules/variable-name/const-only-for-caps/test.ts.lint b/test/rules/variable-name/const-only-for-caps/test.ts.lint new file mode 100644 index 00000000000..8a092a64932 --- /dev/null +++ b/test/rules/variable-name/const-only-for-caps/test.ts.lint @@ -0,0 +1,38 @@ +var snake_case_var; + ~~~~~~~~~~~~~~ [regular-case] + +var camelCaseVar; + +var PascalCaseVar; + ~~~~~~~~~~~~~ [regular-case] + +var UPPER_CASE_VAR; + ~~~~~~~~~~~~~~ [const] + +let snake_case_let; + ~~~~~~~~~~~~~~ [regular-case] + +let camelCaseLet; + +let PascalCaseLet; + ~~~~~~~~~~~~~ [regular-case] + +let UPPER_CASE_LET; + ~~~~~~~~~~~~~~ [const] + +const snake_case_const; + ~~~~~~~~~~~~~~~~ [regular-case] + +const camelCaseConst; + +const PascalCaseConst; + ~~~~~~~~~~~~~~~ [regular-case] + +const UPPER_CASE_CONST; + +class Test { + public static readonly MY_FIELD = 10; +} + +[const]: Only `const` variables may be UPPER_CASE. +[regular-case]: variable name must be in lowerCamelCase or UPPER_CASE diff --git a/test/rules/variable-name/const-only-for-caps/tslint.json b/test/rules/variable-name/const-only-for-caps/tslint.json new file mode 100644 index 00000000000..991cbcb344e --- /dev/null +++ b/test/rules/variable-name/const-only-for-caps/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "variable-name": [true, "require-const-for-all-caps"] + } +}