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

Commit

Permalink
typedef-whitespace: Rewrite and add fixer (#2718)
Browse files Browse the repository at this point in the history
[new-fixer] `typedef-whitespace`
[enhancement] `typedef-whitespace` added checks for arrow function, call and construct signature
[bugfix] `typedef-whitespace` don't warn for leading whitespace if token is preceded by line break
  • Loading branch information
ajafff authored and nchen63 committed May 12, 2017
1 parent ff99981 commit c768aa2
Show file tree
Hide file tree
Showing 10 changed files with 1,993 additions and 2,015 deletions.
303 changes: 103 additions & 200 deletions src/rules/typedefWhitespaceRule.ts
Expand Up @@ -15,10 +15,16 @@
* limitations under the License.
*/

import { getChildOfKind } from "tsutils";
import * as ts from "typescript";

import * as Lint from "../index";

type Option = "nospace" | "onespace" | "space";
type OptionType = "call-signature" | "index-signature" | "parameter" | "property-declaration" | "variable-declaration";
type OptionInput = Partial<Record<OptionType, Option>>;
type Options = Partial<Record<"left" | "right", OptionInput>>;

/* tslint:disable:object-literal-sort-keys */
const SPACE_OPTIONS = {
type: "string",
Expand Down Expand Up @@ -46,7 +52,7 @@ export class Rule extends Lint.Rules.AbstractRule {
Two arguments which are both objects.
The first argument specifies how much space should be to the _left_ of a typedef colon.
The second argument specifies how much space should be to the _right_ of a typedef colon.
Each key should have a value of \`"space"\` or \`"nospace"\`.
Each key should have a value of \`"onespace"\`, \`"space"\` or \`"nospace"\`.
Possible keys are:
* \`"call-signature"\` checks return type of functions.
Expand Down Expand Up @@ -80,225 +86,122 @@ export class Rule extends Lint.Rules.AbstractRule {
],
type: "typescript",
typescriptOnly: true,
hasFix: true,
};
/* tslint:enable:object-literal-sort-keys */

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new TypedefWhitespaceWalker(sourceFile, this.getOptions()));
public static FAILURE_STRING(option: string, location: "before" | "after", type: string) {
return `expected ${option} ${location} colon in ${type}`;
}
}

class TypedefWhitespaceWalker extends Lint.RuleWalker {
private static getColonPosition(node: ts.Node) {
const colon = Lint.childOfKind(node, ts.SyntaxKind.ColonToken);
return colon === undefined ? undefined : colon.getStart();
}

public visitFunctionDeclaration(node: ts.FunctionDeclaration) {
this.checkSpace("call-signature", node, node.type);
super.visitFunctionDeclaration(node);
}

public visitFunctionExpression(node: ts.FunctionExpression) {
this.checkSpace("call-signature", node, node.type);
super.visitFunctionExpression(node);
}

public visitGetAccessor(node: ts.AccessorDeclaration) {
this.checkSpace("call-signature", node, node.type);
super.visitGetAccessor(node);
}

public visitIndexSignatureDeclaration(node: ts.IndexSignatureDeclaration) {
this.checkSpace("index-signature", node, node.type);
super.visitIndexSignatureDeclaration(node);
}

public visitMethodDeclaration(node: ts.MethodDeclaration) {
this.checkSpace("call-signature", node, node.type);
super.visitMethodDeclaration(node);
}

public visitMethodSignature(node: ts.SignatureDeclaration) {
this.checkSpace("call-signature", node, node.type);
super.visitMethodSignature(node);
}

public visitParameterDeclaration(node: ts.ParameterDeclaration) {
this.checkSpace("parameter", node, node.type);
super.visitParameterDeclaration(node);
}

public visitPropertyDeclaration(node: ts.PropertyDeclaration) {
this.checkSpace("property-declaration", node, node.type);
super.visitPropertyDeclaration(node);
}

public visitPropertySignature(node: ts.PropertyDeclaration) {
this.checkSpace("property-declaration", node, node.type);
super.visitPropertySignature(node);
}

public visitSetAccessor(node: ts.AccessorDeclaration) {
this.checkSpace("call-signature", node, node.type);
super.visitSetAccessor(node);
}

public visitVariableDeclaration(node: ts.VariableDeclaration) {
this.checkSpace("variable-declaration", node, node.type);
super.visitVariableDeclaration(node);
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const args = this.ruleArguments as Array<OptionInput | undefined>;
const options = {
left: args[0],
right: args[1],
};
return this.applyWithWalker(new TypedefWhitespaceWalker(sourceFile, this.ruleName, options));
}
}

public checkSpace(option: string, node: ts.Node, typeNode: ts.TypeNode | ts.StringLiteral | undefined) {
if (this.hasOption(option) && typeNode != null) {
const colonPosition = TypedefWhitespaceWalker.getColonPosition(node);

if (colonPosition !== undefined) {
const scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, node.getText());

this.checkLeft(option, node, scanner, colonPosition);
this.checkRight(option, node, scanner, colonPosition);
class TypedefWhitespaceWalker extends Lint.AbstractWalker<Options> {
public walk(sourceFile: ts.SourceFile) {
const cb = (node: ts.Node): void => {
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.ArrowFunction:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
case ts.SyntaxKind.MethodSignature:
case ts.SyntaxKind.ConstructSignature:
case ts.SyntaxKind.CallSignature:
this.checkSpace(node as ts.FunctionLikeDeclaration | ts.SignatureDeclaration, "call-signature");
break;
case ts.SyntaxKind.IndexSignature:
this.checkSpace(node as ts.IndexSignatureDeclaration, "index-signature");
break;
case ts.SyntaxKind.VariableDeclaration:
this.checkSpace(node as ts.VariableDeclaration, "variable-declaration");
break;
case ts.SyntaxKind.Parameter:
this.checkSpace(node as ts.ParameterDeclaration, "parameter");
break;
case ts.SyntaxKind.PropertySignature:
case ts.SyntaxKind.PropertyDeclaration:
this.checkSpace(node as ts.PropertyDeclaration | ts.PropertySignature, "property-declaration");
}
}
}

public hasOption(option: string): boolean {
return this.hasLeftOption(option) || this.hasRightOption(option);
return ts.forEachChild(node, cb);
};
return ts.forEachChild(sourceFile, cb);
}

private hasLeftOption(option: string): boolean {
const allOptions = this.getOptions() as any[];

if (allOptions == null || allOptions.length === 0) {
return false;
private checkSpace(node: ts.FunctionLikeDeclaration | ts.SignatureDeclaration | ts.VariableLikeDeclaration, key: OptionType) {
if (node.type === undefined) {
return;
}

const options = allOptions[0] as { [key: string]: Option };
return options != null && options[option] != null;
}

private hasRightOption(option: string): boolean {
const allOptions = this.getOptions() as any[];

if (allOptions == null || allOptions.length < 2) {
return false;
const {left, right} = this.options;
const colon = getChildOfKind(node, ts.SyntaxKind.ColonToken, this.sourceFile)!;
if (right !== undefined && right[key] !== undefined) {
this.checkRight(colon.end, right[key]!, key);
}

const options = allOptions[1] as { [key: string]: Option };
return options != null && options[option] != null;
}

private getLeftOption(option: string): Option | null {
if (!this.hasLeftOption(option)) {
return null;
if (left !== undefined && left[key] !== undefined) {
this.checkLeft(colon.end - 1, left[key]!, key);
}

const allOptions = this.getOptions() as any[];
const options = allOptions[0] as { [key: string]: Option };
return options[option];
}

private getRightOption(option: string): Option | null {
if (!this.hasRightOption(option)) {
return null;
private checkRight(colonEnd: number, option: Option, key: OptionType) {
let pos = colonEnd;
const {text} = this.sourceFile;
let current = text.charCodeAt(pos);
if (ts.isLineBreak(current)) {
return;
}

const allOptions = this.getOptions() as any[];
const options = allOptions[1] as { [key: string]: Option };
return options[option];
}

private checkLeft(option: string, node: ts.Node, scanner: ts.Scanner, colonPosition: number) {
if (this.hasLeftOption(option)) {
let positionToCheck = colonPosition - 1 - node.getStart();

let hasLeadingWhitespace: boolean;
if (positionToCheck < 0) {
hasLeadingWhitespace = false;
} else {
scanner.setTextPos(positionToCheck);
hasLeadingWhitespace = scanner.scan() === ts.SyntaxKind.WhitespaceTrivia;
}

positionToCheck = colonPosition - 2 - node.getStart();

let hasSeveralLeadingWhitespaces: boolean;
if (positionToCheck < 0) {
hasSeveralLeadingWhitespaces = false;
} else {
scanner.setTextPos(positionToCheck);
hasSeveralLeadingWhitespaces = hasLeadingWhitespace &&
scanner.scan() === ts.SyntaxKind.WhitespaceTrivia;
}

const optionValue = this.getLeftOption(option);
const message = `expected ${optionValue} before colon in ${option}`;
this.performFailureCheck(
optionValue!,
hasLeadingWhitespace,
hasSeveralLeadingWhitespaces,
colonPosition - 1,
message,
);
while (ts.isWhiteSpaceSingleLine(current)) {
++pos;
current = text.charCodeAt(pos);
}
return this.validateWhitespace(colonEnd, pos, option, "after", key);
}

private checkRight(option: string, node: ts.Node, scanner: ts.Scanner, colonPosition: number) {
if (this.hasRightOption(option)) {
let positionToCheck = colonPosition + 1 - node.getStart();

// Don't enforce trailing spaces on newlines
// (https://github.com/palantir/tslint/issues/1354)
scanner.setTextPos(positionToCheck);
const kind = scanner.scan();
if (kind === ts.SyntaxKind.NewLineTrivia) {
return;
}

let hasTrailingWhitespace: boolean;
if (positionToCheck >= node.getWidth()) {
hasTrailingWhitespace = false;
} else {
hasTrailingWhitespace = kind === ts.SyntaxKind.WhitespaceTrivia;
}

positionToCheck = colonPosition + 2 - node.getStart();

let hasSeveralTrailingWhitespaces: boolean;
if (positionToCheck >= node.getWidth()) {
hasSeveralTrailingWhitespaces = false;
} else {
scanner.setTextPos(positionToCheck);
hasSeveralTrailingWhitespaces = hasTrailingWhitespace &&
scanner.scan() === ts.SyntaxKind.WhitespaceTrivia;
}

const optionValue = this.getRightOption(option);
const message = `expected ${optionValue} after colon in ${option}`;
this.performFailureCheck(
optionValue!,
hasTrailingWhitespace,
hasSeveralTrailingWhitespaces,
colonPosition + 1,
message,
);
private checkLeft(colonStart: number, option: Option, key: OptionType) {
let pos = colonStart;
const {text} = this.sourceFile;
let current = text.charCodeAt(pos - 1);
while (ts.isWhiteSpaceSingleLine(current)) {
--pos;
current = text.charCodeAt(pos - 1);
}
}

private performFailureCheck(optionValue: Option, hasWS: boolean, hasSeveralWS: boolean, failurePos: number, message: string) {
// has several spaces but should have one or none
let isFailure = hasSeveralWS &&
(optionValue === "onespace" || optionValue === "nospace");
// has at least one space but should have none
isFailure = isFailure || hasWS && optionValue === "nospace";
// has no space but should have at least one
isFailure = isFailure || !hasWS &&
(optionValue === "onespace" || optionValue === "space");

if (isFailure) {
this.addFailureAt(failurePos, 1, message);
if (ts.isLineBreak(current)) {
return;
}
return this.validateWhitespace(pos, colonStart, option, "before", key);
}

private validateWhitespace(start: number, end: number, option: Option, location: "before" | "after", key: OptionType) {
switch (option) {
case "nospace":
if (start !== end) {
this.addFailure(start, end, Rule.FAILURE_STRING(option, location, key), Lint.Replacement.deleteFromTo(start, end));
}
break;
case "space":
if (start === end) {
this.addFailure(end, end, Rule.FAILURE_STRING(option, location, key), Lint.Replacement.appendText(end, " "));
}
break;
case "onespace":
switch (end - start) {
case 0:
this.addFailure(end, end, Rule.FAILURE_STRING(option, location, key), Lint.Replacement.appendText(end, " "));
break;
case 1:
break;
default:
this.addFailure(start + 1, end, Rule.FAILURE_STRING(option, location, key),
Lint.Replacement.deleteFromTo(start + 1, end));
}
}
}
}

type Option = "nospace" | "onespace" | "space";

0 comments on commit c768aa2

Please sign in to comment.