vrsource-tslint-rules
Version:
Extension rules for tslint
443 lines (372 loc) • 16.8 kB
text/typescript
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(<ts.Identifier> 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(<ts.VariableDeclaration>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)
? (<ts.VariableDeclaration> node).parent
: (<ts.VariableStatement> 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.
*/