-
Notifications
You must be signed in to change notification settings - Fork 887
Refactor variable-name rule #2355
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,11 +15,14 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
// tslint:disable object-literal-sort-keys | ||
|
||
import * as ts from "typescript"; | ||
|
||
import * as Lint from "../index"; | ||
|
||
const BANNED_KEYWORDS = ["any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"]; | ||
const BANNED_KEYWORDS = new Set(["any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"]); | ||
const bannedKeywordsStr = Array.from(BANNED_KEYWORDS).map((kw) => `\`${kw}\``).join(", "); | ||
|
||
const OPTION_LEADING_UNDERSCORE = "allow-leading-underscore"; | ||
const OPTION_TRAILING_UNDERSCORE = "allow-trailing-underscore"; | ||
|
@@ -28,7 +31,6 @@ const OPTION_CHECK_FORMAT = "check-format"; | |
const OPTION_ALLOW_PASCAL_CASE = "allow-pascal-case"; | ||
|
||
export class Rule extends Lint.Rules.AbstractRule { | ||
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: "variable-name", | ||
description: "Checks variable names for various errors.", | ||
|
@@ -39,8 +41,8 @@ export class Rule extends Lint.Rules.AbstractRule { | |
* \`"${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_ALLOW_PASCAL_CASE}"\` allows PascalCase in addition to camelCase. | ||
* \`"${OPTION_BAN_KEYWORDS}"\`: disallows the use of certain TypeScript keywords (\`any\`, \`Number\`, \`number\`, \`String\`, | ||
\`string\`, \`Boolean\`, \`boolean\`, \`undefined\`) as variable or parameter names.`, | ||
* \`"${OPTION_BAN_KEYWORDS}"\`: disallows the use of certain TypeScript keywords as variable or parameter names. | ||
* These are: ${bannedKeywordsStr}`, | ||
options: { | ||
type: "array", | ||
items: { | ||
|
@@ -60,130 +62,136 @@ export class Rule extends Lint.Rules.AbstractRule { | |
type: "style", | ||
typescriptOnly: false, | ||
}; | ||
/* tslint:enable:object-literal-sort-keys */ | ||
|
||
public static FORMAT_FAILURE = "variable name must be in camelcase or uppercase"; | ||
public static KEYWORD_FAILURE = "variable name clashes with keyword/type"; | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
const variableNameWalker = new VariableNameWalker(sourceFile, this.getOptions()); | ||
return this.applyWithWalker(variableNameWalker); | ||
return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, parseOptions(this.getOptions().ruleArguments))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and as always, why not pass options as third parameter to |
||
} | ||
} | ||
|
||
class VariableNameWalker extends Lint.RuleWalker { | ||
private shouldBanKeywords: boolean; | ||
private shouldCheckFormat: boolean; | ||
|
||
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { | ||
super(sourceFile, options); | ||
|
||
this.shouldBanKeywords = this.hasOption(OPTION_BAN_KEYWORDS); | ||
interface Options { | ||
banKeywords: boolean; | ||
checkFormat: boolean; | ||
leadingUnderscore: boolean; | ||
trailingUnderscore: boolean; | ||
allowPascalCase: boolean; | ||
} | ||
function parseOptions(ruleArguments: string[]): Options { | ||
const banKeywords = hasOption(OPTION_BAN_KEYWORDS); | ||
return { | ||
banKeywords, | ||
// check variable name formatting by default if no options are specified | ||
this.shouldCheckFormat = !this.shouldBanKeywords || this.hasOption(OPTION_CHECK_FORMAT); | ||
checkFormat: !banKeywords || hasOption(OPTION_CHECK_FORMAT), | ||
leadingUnderscore: hasOption(OPTION_LEADING_UNDERSCORE), | ||
trailingUnderscore: hasOption(OPTION_TRAILING_UNDERSCORE), | ||
allowPascalCase: hasOption(OPTION_ALLOW_PASCAL_CASE), | ||
}; | ||
|
||
function hasOption(name: string): boolean { | ||
return ruleArguments.indexOf(name) !== -1; | ||
} | ||
} | ||
|
||
public visitBindingElement(node: ts.BindingElement) { | ||
if (node.name.kind === ts.SyntaxKind.Identifier) { | ||
const identifier = node.name as ts.Identifier; | ||
this.handleVariableNameKeyword(identifier); | ||
// A destructuring pattern that does not rebind an expression is always an alias, e.g. `var {Foo} = ...;`. | ||
// Only check if the name is rebound (`var {Foo: bar} = ...;`). | ||
if (node.parent !== undefined && node.parent.kind !== ts.SyntaxKind.ObjectBindingPattern || node.propertyName) { | ||
this.handleVariableNameFormat(identifier, node.initializer); | ||
function walk(ctx: Lint.WalkContext<void>, options: Options): void { | ||
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.BindingElement: { | ||
const { initializer, name, propertyName } = node as ts.BindingElement; | ||
if (name.kind === ts.SyntaxKind.Identifier) { | ||
handleVariableNameKeyword(name); | ||
// A destructuring pattern that does not rebind an expression is always an alias, e.g. `var {Foo} = ...;`. | ||
// Only check if the name is rebound (`var {Foo: bar} = ...;`). | ||
if (node.parent!.kind !== ts.SyntaxKind.ObjectBindingPattern || propertyName) { | ||
handleVariableNameFormat(name, initializer); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
super.visitBindingElement(node); | ||
} | ||
|
||
public visitParameterDeclaration(node: ts.ParameterDeclaration) { | ||
if (node.name.kind === ts.SyntaxKind.Identifier) { | ||
const identifier = node.name as ts.Identifier; | ||
this.handleVariableNameFormat(identifier, undefined /* parameters may not alias */); | ||
this.handleVariableNameKeyword(identifier); | ||
case ts.SyntaxKind.VariableStatement: | ||
// skip 'declare' keywords | ||
if (Lint.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); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
super.visitParameterDeclaration(node); | ||
} | ||
|
||
public visitPropertyDeclaration(node: ts.PropertyDeclaration) { | ||
if (node.name != null && node.name.kind === ts.SyntaxKind.Identifier) { | ||
const identifier = node.name as ts.Identifier; | ||
this.handleVariableNameFormat(identifier, node.initializer); | ||
// do not check property declarations for keywords, they are allowed to be keywords | ||
} | ||
super.visitPropertyDeclaration(node); | ||
} | ||
return ts.forEachChild(node, cb); | ||
}); | ||
|
||
public visitVariableDeclaration(node: ts.VariableDeclaration) { | ||
if (node.name.kind === ts.SyntaxKind.Identifier) { | ||
const identifier = node.name as ts.Identifier; | ||
this.handleVariableNameFormat(identifier, node.initializer); | ||
this.handleVariableNameKeyword(identifier); | ||
function handleVariableNameFormat(name: ts.Identifier, initializer?: ts.Expression): void { | ||
if (!options.checkFormat) { | ||
return; | ||
} | ||
super.visitVariableDeclaration(node); | ||
} | ||
|
||
public visitVariableStatement(node: ts.VariableStatement) { | ||
// skip 'declare' keywords | ||
if (!Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)) { | ||
super.visitVariableStatement(node); | ||
const { text } = name; | ||
if (initializer && isAlias(text, initializer)) { | ||
return; | ||
} | ||
} | ||
|
||
private isAlias(name: ts.Identifier, initializer: ts.Expression): boolean { | ||
if (initializer.kind === ts.SyntaxKind.PropertyAccessExpression) { | ||
return (initializer as ts.PropertyAccessExpression).name.text === name.text; | ||
} else if (initializer.kind === ts.SyntaxKind.Identifier) { | ||
return (initializer as ts.Identifier).text === name.text; | ||
if (!isCamelCase(text, options) && !isUpperCase(text)) { | ||
ctx.addFailureAtNode(name, Rule.FORMAT_FAILURE); | ||
} | ||
return false; | ||
} | ||
|
||
private handleVariableNameFormat(name: ts.Identifier, initializer?: ts.Expression) { | ||
const variableName = name.text; | ||
|
||
if (initializer && this.isAlias(name, initializer)) { | ||
return; | ||
} | ||
|
||
if (this.shouldCheckFormat && !this.isCamelCase(variableName) && !isUpperCase(variableName)) { | ||
this.addFailureAtNode(name, Rule.FORMAT_FAILURE); | ||
function handleVariableNameKeyword(name: ts.Identifier): void { | ||
if (options.banKeywords && BANNED_KEYWORDS.has(name.text)) { | ||
ctx.addFailureAtNode(name, Rule.KEYWORD_FAILURE); | ||
} | ||
} | ||
} | ||
|
||
private handleVariableNameKeyword(name: ts.Identifier) { | ||
const variableName = name.text; | ||
|
||
if (this.shouldBanKeywords && BANNED_KEYWORDS.indexOf(variableName) !== -1) { | ||
this.addFailureAtNode(name, Rule.KEYWORD_FAILURE); | ||
} | ||
function isAlias(name: string, initializer: ts.Expression): boolean { | ||
switch (initializer.kind) { | ||
case ts.SyntaxKind.PropertyAccessExpression: | ||
return (initializer as ts.PropertyAccessExpression).name.text === name; | ||
case ts.SyntaxKind.Identifier: | ||
return (initializer as ts.Identifier).text === name; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
private isCamelCase(name: string) { | ||
const firstCharacter = name.charAt(0); | ||
const lastCharacter = name.charAt(name.length - 1); | ||
const middle = name.substr(1, name.length - 2); | ||
function isCamelCase(name: string, options: Options): boolean { | ||
const firstCharacter = name.charAt(0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const lastCharacter = name.charAt(name.length - 1); | ||
const middle = name.substr(1, name.length - 2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be simplified to |
||
|
||
if (name.length <= 0) { | ||
return true; | ||
} | ||
if (!this.hasOption(OPTION_LEADING_UNDERSCORE) && firstCharacter === "_") { | ||
return false; | ||
} | ||
if (!this.hasOption(OPTION_TRAILING_UNDERSCORE) && lastCharacter === "_") { | ||
return false; | ||
} | ||
if (!this.hasOption(OPTION_ALLOW_PASCAL_CASE) && !isLowerCase(firstCharacter)) { | ||
return false; | ||
} | ||
return middle.indexOf("_") === -1; | ||
if (name.length <= 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know that check was there before, but when would this ever be true? |
||
return true; | ||
} | ||
if (!options.leadingUnderscore && firstCharacter === "_") { | ||
return false; | ||
} | ||
if (!options.trailingUnderscore && lastCharacter === "_") { | ||
return false; | ||
} | ||
if (!options.allowPascalCase && !isLowerCase(firstCharacter)) { | ||
return false; | ||
} | ||
return middle.indexOf("_") === -1; | ||
} | ||
|
||
function isLowerCase(name: string) { | ||
function isLowerCase(name: string): boolean { | ||
return name === name.toLowerCase(); | ||
} | ||
|
||
function isUpperCase(name: string) { | ||
function isUpperCase(name: string): boolean { | ||
return name === name.toUpperCase(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why initialize a Set with an array and the convert it back to an array? Wouldn't it make more sense to just use the same array for both?
Also IIRC a set does not guarantee any order, which could separate
Number
andnumber
in the output for example.