Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Commit

Permalink
Added space-before-function-paren rule (#1897)
Browse files Browse the repository at this point in the history
  • Loading branch information
cameron-mcateer authored and nchen63 committed Dec 24, 2016
1 parent ea44b40 commit 60e55d6
Show file tree
Hide file tree
Showing 13 changed files with 760 additions and 0 deletions.
193 changes: 193 additions & 0 deletions src/rules/spaceBeforeFunctionParenRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as ts from "typescript";
import * as Lint from "../index";

const ALWAYS_OR_NEVER = {
enum: ["always", "never"],
type: "string",
};

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
description: "Require or disallow a space before function parenthesis",
optionExamples: [
`true`,
`[true, "always"]`,
`[true, "never"]`,
`[true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}]`,
],
options: {
properties: {
anonymous: ALWAYS_OR_NEVER,
asyncArrow: ALWAYS_OR_NEVER,
constructor: ALWAYS_OR_NEVER,
method: ALWAYS_OR_NEVER,
named: ALWAYS_OR_NEVER,
},
type: "object",
},
optionsDescription: Lint.Utils.dedent`
One argument which is an object which may contain the keys \`anonymous\`, \`named\`, and \`asyncArrow\`
These should be set to either \`"always"\` or \`"never"\`.
* \`"anonymous"\` checks before the opening paren in anonymous functions
* \`"named"\` checks before the opening paren in named functions
* \`"asyncArrow"\` checks before the opening paren in async arrow functions
* \`"method"\` checks before the opening paren in class methods
* \`"constructor"\` checks before the opening paren in class constructors
`,
ruleName: "space-before-function-paren",
type: "style",
typescriptOnly: false,
};
public static INVALID_WHITESPACE_ERROR = "Spaces before function parens are disallowed";
public static MISSING_WHITESPACE_ERROR = "Missing whitespace before function parens";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new FunctionWalker(sourceFile, this.getOptions()));
}
}

type OptionName = "anonymous" | "asyncArrow" | "constructor" | "method" | "named";

interface CachedOptions {
anonymous?: string;
asyncArrow?: string;
constructor?: string;
method?: string;
named?: string;
}

class FunctionWalker extends Lint.RuleWalker {
private scanner: ts.Scanner;
// assign constructor now to avoid typescript assuming its a function type
private cachedOptions: CachedOptions = {constructor: undefined};

constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);
this.scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, sourceFile.text);
this.cacheOptions();
}

protected visitArrowFunction(node: ts.FunctionLikeDeclaration): void {
const option = this.getOption("asyncArrow");
const syntaxList = this.getChildOfType(node, ts.SyntaxKind.SyntaxList);
const isAsyncArrow = syntaxList.getStart() === node.getStart() && syntaxList.getText() === "async";
const openParen = isAsyncArrow ? this.getChildOfType(node, ts.SyntaxKind.OpenParenToken) : undefined;
this.evaluateRuleAt(openParen, option);

super.visitArrowFunction(node);
}

protected visitConstructorDeclaration(node: ts.ConstructorDeclaration): void {
const option = this.getOption("constructor");
const openParen = this.getChildOfType(node, ts.SyntaxKind.OpenParenToken);
this.evaluateRuleAt(openParen, option);

super.visitConstructorDeclaration(node);
}

protected visitFunctionDeclaration(node: ts.FunctionDeclaration): void {
this.visitFunction(node);
super.visitFunctionDeclaration(node);
}

protected visitFunctionExpression(node: ts.FunctionExpression): void {
this.visitFunction(node);
super.visitFunctionExpression(node);
}

protected visitMethodDeclaration(node: ts.MethodDeclaration): void {
this.visitMethod(node);
super.visitMethodDeclaration(node);
}

protected visitMethodSignature(node: ts.SignatureDeclaration): void {
this.visitMethod(node);
super.visitMethodSignature(node);
}

private cacheOptions(): void {
const allOptions = this.getOptions();
const options = allOptions[0];
const optionNames: OptionName[] = ["anonymous", "asyncArrow", "constructor", "method", "named"];

optionNames.forEach((optionName) => {
switch (options) {
case undefined:
case "always":
this.cachedOptions[optionName] = "always";
break;

case "never":
this.cachedOptions[optionName] = "never";
break;

default:
this.cachedOptions[optionName] = options[optionName];
}
});
}

private getOption(optionName: OptionName): string {
return this.cachedOptions[optionName];
}

private getChildOfType(node: ts.Node, kind: ts.SyntaxKind): ts.Node {
const filtered = node.getChildren().filter((child: ts.Node): boolean => child.kind === kind);
return filtered[0];
}

private evaluateRuleAt(openParen: ts.Node, option: string): void {
if (openParen === undefined) {
return;
}

const hasSpace = this.isSpaceAt(openParen.getStart() - 1);
let failure: Lint.RuleFailure;

if (hasSpace && option === "never") {
failure = this.createInvalidWhitespaceFailure(openParen.getStart() - 1);
} else if (!hasSpace && option === "always") {
failure = this.createMissingWhitespaceFailure(openParen.getStart());
}

if (failure !== undefined) {
this.addFailure(failure);
}
}

private isSpaceAt(textPos: number): boolean {
this.scanner.setTextPos(textPos);
const prevTokenKind = this.scanner.scan();
return prevTokenKind === ts.SyntaxKind.WhitespaceTrivia;
}

private visitFunction(node: ts.Node): void {
const identifier = this.getChildOfType(node, ts.SyntaxKind.Identifier);
const hasIdentifier = identifier !== undefined && (identifier.getEnd() !== identifier.getStart());
const optionName = hasIdentifier ? "named" : "anonymous";
const option = this.getOption(optionName);
const openParen = this.getChildOfType(node, ts.SyntaxKind.OpenParenToken);
this.evaluateRuleAt(openParen, option);
}

private visitMethod(node: ts.Node): void {
const option = this.getOption("method");
const openParen = this.getChildOfType(node, ts.SyntaxKind.OpenParenToken);
this.evaluateRuleAt(openParen, option);
}

private createInvalidWhitespaceFailure(pos: number): Lint.RuleFailure {
const fix = new Lint.Fix(Rule.metadata.ruleName, [
this.deleteText(pos, 1),
]);
return this.createFailure(pos, 1, Rule.INVALID_WHITESPACE_ERROR, fix);
}

private createMissingWhitespaceFailure(pos: number): Lint.RuleFailure {
const fix = new Lint.Fix(Rule.metadata.ruleName, [
this.appendText(pos, " "),
]);
return this.createFailure(pos, 1, Rule.MISSING_WHITESPACE_ERROR, fix);
}
}
36 changes: 36 additions & 0 deletions test/rules/space-before-function-paren/always/test.js.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Anonymous
function() {}
~ [space-before-function-paren]
function(): void {
~ [space-before-function-paren]
}
function(a: string, cb: ()=>{}): void {}
~ [space-before-function-paren]

var f = function() {};
~ [space-before-function-paren]
var f = function(): void {
~ [space-before-function-paren]
};
var f = function(a: string, cb: ()=>{}): void {};
~ [space-before-function-paren]


// Named
function foobar(){}
~ [space-before-function-paren]
function foobar(): void{
~ [space-before-function-paren]
}
function foobar(a: string, cb: ()=>{}): void{}
~ [space-before-function-paren]

var f = function foobar(){};
~ [space-before-function-paren]
var f = function foobar(): void{
~ [space-before-function-paren]
};
var f = function foobar(a: string, cb: ()=>{}): void{};
~ [space-before-function-paren]

[space-before-function-paren]: Missing whitespace before function parens
59 changes: 59 additions & 0 deletions test/rules/space-before-function-paren/always/test.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Anonymous
function () {}
function (): void {
}
function (a: string, cb: ()=>{}): void {}

var f = function () {};
var f = function (): void {
};
var f = function (a: string, cb: ()=>{}): void {};


// Named
function foobar (){}
function foobar (): void{
}
function foobar (a: string, cb: ()=>{}): void{}

var f = function foobar (){};
var f = function foobar (): void{
};
var f = function foobar (a: string, cb: ()=>{}): void{};


// Async Arrow
// ignore
() => {};
var arrow = () => {};

async () => {};
var arrow = async () => {};


// Method
interface IMyInterface {
one ();
two (): void;
three (a: string, cb: ()=>{}): void;
}
class MyClass {
one () {}
two (): void {
}
three (a: string, cb: ()=>{}): void {}
}


// Constructor
class MyClass {
constructor () {}
}
class MyClass {
constructor (): void {
}
}
class MyClass {
constructor (a: string, cb: ()=>{}): void {}
}

83 changes: 83 additions & 0 deletions test/rules/space-before-function-paren/always/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Anonymous
function() {}
~ [space-before-function-paren]
function(): void {
~ [space-before-function-paren]
}
function(a: string, cb: ()=>{}): void {}
~ [space-before-function-paren]

var f = function() {};
~ [space-before-function-paren]
var f = function(): void {
~ [space-before-function-paren]
};
var f = function(a: string, cb: ()=>{}): void {};
~ [space-before-function-paren]


// Named
function foobar(){}
~ [space-before-function-paren]
function foobar(): void{
~ [space-before-function-paren]
}
function foobar(a: string, cb: ()=>{}): void{}
~ [space-before-function-paren]

var f = function foobar(){};
~ [space-before-function-paren]
var f = function foobar(): void{
~ [space-before-function-paren]
};
var f = function foobar(a: string, cb: ()=>{}): void{};
~ [space-before-function-paren]


// Async Arrow
// ignore
() => {};
var arrow = () => {};

async() => {};
~ [space-before-function-paren]
var arrow = async() => {};
~ [space-before-function-paren]


// Method
interface IMyInterface {
one();
~ [space-before-function-paren]
two(): void;
~ [space-before-function-paren]
three(a: string, cb: ()=>{}): void;
~ [space-before-function-paren]
}
class MyClass {
one() {}
~ [space-before-function-paren]
two(): void {
~ [space-before-function-paren]
}
three(a: string, cb: ()=>{}): void {}
~ [space-before-function-paren]
}


// Constructor
class MyClass {
constructor() {}
~ [space-before-function-paren]
}
class MyClass {
constructor(): void {
~ [space-before-function-paren]
}
}
class MyClass {
constructor(a: string, cb: ()=>{}): void {}
~ [space-before-function-paren]
}

[space-before-function-paren]: Missing whitespace before function parens
8 changes: 8 additions & 0 deletions test/rules/space-before-function-paren/always/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"rules": {
"space-before-function-paren": true
},
"jsRules": {
"space-before-function-paren": [true, "always"]
}
}

0 comments on commit 60e55d6

Please sign in to comment.