import * as ts from "typescript"; import * as Lint from "tslint"; /** KEEP IN SYNC WITH README.md ## ext-variable-name This rule provides extensive support for customizing allowable variable names for a wide variety of variable tags. The rule is configured by setting up a list of sub-rules that specify the tags of variables to check and the checks to perform on the variable's name. The sub-rules are checked in order and the first one that matches the tags of variable being checked is the only one that is used. An example set of sub-rules for an example coding standard is shown below. ```json "ext-variable-name": [ true, ["class", "pascal"], ["interface", "pascal", {"regex": "^I.*$"}], ["parameter", "camel"], ["property", "static", "camel", {"regex": "^s.*$"}], ["property", "private", "camel", "allow-leading-underscore"], ["property", "protected", "camel", "allow-leading-underscore"], ["variable", "local", "snake"], ["variable", "const", "upper"], ["variable", "camel", {"regex": "^g.*$"}], ["method", "private", "camel", "allow-leading-underscore"], ["method", "protected", "camel", "allow-leading-underscore"], ["function", "camel"], ["default", "camel"] ] ``` Allowed tags for variables: * primary (choose one): * class, interface, parameter, property, method, function, variable * modifiers (choose zero or more): * local, const, static, public, protected, private note: If any tags is added to a sub-rule then **all** must match the variable. Checks allowed: * One of: * "camel": Require variables to use camelCaseVariables * "snake": Require variables to use snake_case_variables * "pascal": Require variables to use PascalCaseVariables * "upper": Require variables to use UPPER_CASE_VARIABLES * "allow-leading-underscore": permits the variable to have a leading underscore * "allow-trailing-underscore": permits the variable to have a trailing underscore * "require-leading-underscore": requires the variable to have a leading underscore * "require-trailing-underscore": requires the variable to have a trailing underscore * "ban-keywords": bans a list of language keywords from being used * {"regex": "^.*$"}: checks the variable name against the given regex */ const CLASS_TAG = "class"; const INTERFACE_TAG = "interface"; const PARAMETER_TAG = "parameter"; const PROPERTY_TAG = "property"; const METHOD_TAG = "method"; const FUNCTION_TAG = "function"; const VARIABLE_TAG = "variable"; const LOCAL_TAG = "local"; const STATIC_TAG = "static"; const CONST_TAG = "const"; const PUBLIC_TAG = "public"; const PROTECTED_TAG = "protected"; const PRIVATE_TAG = "private"; const VALID_VAR_TAGS = [CLASS_TAG, INTERFACE_TAG, PARAMETER_TAG, PROPERTY_TAG, METHOD_TAG, FUNCTION_TAG, VARIABLE_TAG, LOCAL_TAG, STATIC_TAG, CONST_TAG, PUBLIC_TAG, PROTECTED_TAG, PRIVATE_TAG]; const PASCAL_OPTION = "pascal"; const CAMEL_OPTION = "camel"; const SNAKE_OPTION = "snake"; const UPPER_OPTION = "upper"; const ALLOW_LEADING_UNDERSCORE_OPTION = "allow-leading-underscore"; const ALLOW_TRAILING_UNDERSCORE_OPTION = "allow-trailing-underscore"; const REQUIRE_LEADING_UNDERSCORE_OPTION = "require-leading-underscore"; const REQUIRE_TRAILING_UNDERSCORE_OPTION = "require-trailing-underscore"; const BAN_KEYWORDS_OPTION = "ban-keywords"; const CAMEL_FAIL = "must be in camel case"; const PASCAL_FAIL = "must be in pascal case"; const SNAKE_FAIL = "must be in snake case"; const UPPER_FAIL = "must be in uppercase"; const KEYWORD_FAIL = "name clashes with keyword/type"; const LEADING_FAIL = "name must not have leading underscore"; const TRAILING_FAIL = "name must not have trailing underscore"; const NO_LEADING_FAIL = "name must have leading underscore"; const NO_TRAILING_FAIL = "name must have trailing underscore"; const REGEX_FAIL = "name did not match required regex"; const BANNED_KEYWORDS = ["any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"]; /** * Configured with details needed to check a specific variable type. */ class VariableChecker { public varTags: string[]; public caseCheck: string = ""; public allowLeadingUnderscore: boolean = false; public allowTrailingUnderscore: boolean = false; public requireLeadingUnderscore: boolean = false; public requireTrailingUnderscore: boolean = false; public banKeywords: boolean = false; public regex: RegExp | null = null; constructor(opts: any[]) { this.varTags = opts.filter(v => contains(VALID_VAR_TAGS, v)); if (contains(opts, PASCAL_OPTION)) { this.caseCheck = PASCAL_OPTION; } else if (contains(opts, CAMEL_OPTION)) { this.caseCheck = CAMEL_OPTION; } else if (contains(opts, SNAKE_OPTION)) { this.caseCheck = SNAKE_OPTION; } else if (contains(opts, UPPER_OPTION)) { this.caseCheck = UPPER_OPTION; } this.allowLeadingUnderscore = contains(opts, ALLOW_LEADING_UNDERSCORE_OPTION); this.allowTrailingUnderscore = contains(opts, ALLOW_TRAILING_UNDERSCORE_OPTION); this.requireLeadingUnderscore = contains(opts, REQUIRE_LEADING_UNDERSCORE_OPTION); this.requireTrailingUnderscore = contains(opts, REQUIRE_TRAILING_UNDERSCORE_OPTION); this.banKeywords = contains(opts, BAN_KEYWORDS_OPTION); opts.forEach((opt) => { if (opt.regex !== undefined) { this.regex = new RegExp(opt.regex); } }); } /** * return true if all of our tags are all in the input tags * (ie. we match if we dont have a tag that prevents it) */ public requiredTagsFound(proposedTags: string[]) { let matches = true; this.varTags.forEach((tag) => { if (!contains(proposedTags, tag)) { matches = false; } }); return matches; } protected failMessage(failMessage: string, tag: string) { return tag[0].toUpperCase() + tag.substr(1) + " " + failMessage; } public checkName(name: ts.Identifier, walker: Lint.RuleWalker, tag: string) { let variableName = name.text; const firstCharacter = variableName[0]; const lastCharacter = variableName[variableName.length - 1]; // start with regex test before we potentially strip off underscores if ((this.regex !== null) && !variableName.match(this.regex)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(REGEX_FAIL, tag))); } // check banned words before we potentially strip off underscores if (this.banKeywords && contains(BANNED_KEYWORDS, variableName)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(KEYWORD_FAIL, tag))); } // check leading and trailing underscore if ("_" === firstCharacter) { if (!this.allowLeadingUnderscore && !this.requireLeadingUnderscore) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(LEADING_FAIL, tag))); } variableName = variableName.slice(1); } else if (this.requireLeadingUnderscore) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(NO_LEADING_FAIL, tag))); } if (("_" === lastCharacter) && (variableName.length > 0)) { if (!this.allowTrailingUnderscore && !this.requireTrailingUnderscore) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(TRAILING_FAIL, tag))); } variableName = variableName.slice(0, -1); } else if (this.requireTrailingUnderscore) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(NO_TRAILING_FAIL, tag))); } // run case checks if ((PASCAL_OPTION === this.caseCheck) && !isPascalCased(variableName)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(PASCAL_FAIL, tag))); } else if ((CAMEL_OPTION === this.caseCheck) && !isCamelCase(variableName)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(CAMEL_FAIL, tag))); } else if ((SNAKE_OPTION === this.caseCheck) && !isSnakeCase(variableName)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(SNAKE_FAIL, tag))); } else if ((UPPER_OPTION === this.caseCheck) && !isUpperCase(variableName)) { walker.addFailure(walker.createFailure(name.getStart(), name.getWidth(), this.failMessage(UPPER_FAIL, tag))); } } } class VariableNameWalker extends Lint.RuleWalker { public checkers: VariableChecker[] = []; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { super(sourceFile, options); let sub_rules = options.ruleArguments; sub_rules.forEach((rule_opts: any[]) => { this.checkers.push(new VariableChecker(rule_opts)); }); } public visitClassDeclaration(node: ts.ClassDeclaration) { // classes declared as default exports will be unnamed this.checkName(node, CLASS_TAG); super.visitClassDeclaration(node); } public visitMethodDeclaration(node: ts.MethodDeclaration) { this.checkName(node, METHOD_TAG); super.visitMethodDeclaration(node); } public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { this.checkName(node, INTERFACE_TAG); super.visitInterfaceDeclaration(node); } // what is this? public visitBindingElement(node: ts.BindingElement) { this.checkName(node, VARIABLE_TAG); super.visitBindingElement(node); } public visitParameterDeclaration(node: ts.ParameterDeclaration) { const parameterProperty: boolean = Lint.hasModifier(node.modifiers, ts.SyntaxKind.PublicKeyword) || Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword) || Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword); this.checkName(node, parameterProperty ? PROPERTY_TAG : PARAMETER_TAG); super.visitParameterDeclaration(node); } public visitPropertyDeclaration(node: ts.PropertyDeclaration) { this.checkName(node, PROPERTY_TAG); super.visitPropertyDeclaration(node); } public visitSetAccessor(node: ts.SetAccessorDeclaration) { this.checkName(node, PROPERTY_TAG); super.visitSetAccessor(node); } public visitGetAccessor(node: ts.GetAccessorDeclaration) { this.checkName(node, PROPERTY_TAG); super.visitGetAccessor(node); } public visitVariableDeclaration(node: ts.VariableDeclaration) { this.checkName(node, VARIABLE_TAG); super.visitVariableDeclaration(node); } public visitVariableStatement(node: ts.VariableStatement) { // skip 'declare' keywords if (!Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)) { super.visitVariableStatement(node); } } public visitFunctionDeclaration(node: ts.FunctionDeclaration) { this.checkName(node, FUNCTION_TAG); super.visitFunctionDeclaration(node); } protected checkName(node: ts.NamedDeclaration, tag: string) { if (node.name && node.name.kind === ts.SyntaxKind.Identifier) { const matching_checker = this.getMatchingChecker(this.getNodeTags(node, tag)); if (matching_checker !== null) { matching_checker.checkName( node.name, this, tag); } } } protected getMatchingChecker(varTags: string[]): VariableChecker | null { let matching_checkers = this.checkers.filter(checker => checker.requiredTagsFound(varTags)); if (matching_checkers.length > 0) { return matching_checkers[0]; } else { return null; } } protected getNodeTags(node: ts.Node, primaryTag: string): string[] { let tags = [primaryTag]; if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword)) { tags.push(STATIC_TAG); } if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ConstKeyword)) { tags.push(CONST_TAG); } if (primaryTag === PROPERTY_TAG || primaryTag === METHOD_TAG) { if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { tags.push(PRIVATE_TAG); } else if (Lint.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { tags.push(PROTECTED_TAG); } else { // xxx: should fix so only get public when it is a property tags.push(PUBLIC_TAG); } } let nearest_body = nearestBody(node); if (!nearest_body.isSourceFile) { tags.push(LOCAL_TAG); } if (node.kind === ts.SyntaxKind.VariableDeclaration) { if (isConstVariable(node)) { tags.push(CONST_TAG); } } return tags; } } function nearestBody(node: ts.Node): {isSourceFile: boolean, containingBody: ts.Node | undefined} { const VALID_PARENT_TYPES = [ ts.SyntaxKind.SourceFile, ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.Constructor, ]; let ancestor = node.parent; while (ancestor && !contains(VALID_PARENT_TYPES, ancestor.kind)) { ancestor = ancestor.parent; } return { containingBody: ancestor, isSourceFile: (ancestor && ancestor.kind === ts.SyntaxKind.SourceFile) || !ancestor, }; } function isConstVariable(node: ts.VariableDeclaration | ts.VariableStatement): boolean { const parentNode = (node.kind === ts.SyntaxKind.VariableDeclaration) ? ( node).parent : ( node).declarationList; return !parentNode || Lint.isNodeFlagSet(parentNode, ts.NodeFlags.Const); } function isPascalCased(name: string) { if (name.length <= 0) { return true; } const firstCharacter = name.charAt(0); return ((firstCharacter === firstCharacter.toUpperCase()) && name.indexOf("_") === -1); } function isCamelCase(name: string) { const firstCharacter = name.charAt(0); if (name.length <= 0) { return true; } if (!isLowerCase(firstCharacter)) { return false; } return name.indexOf("_") === -1; } function isSnakeCase(name: string) { return isLowerCase(name); } function isLowerCase(name: string) { return name === name.toLowerCase(); } function isUpperCase(name: string) { return name === name.toUpperCase(); } function contains(arr: any[], value: any): boolean { return arr.indexOf(value) !== -1; } export class Rule extends Lint.Rules.AbstractRule { public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { const variableNameWalker = new VariableNameWalker(sourceFile, this.getOptions()); return this.applyWithWalker(variableNameWalker); } } /** * Original version based upon variable-name rule: * * @license * 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. */