@motorcycle/tslint
Version:
TSLint for Motorcycle.js Projects
211 lines (156 loc) • 6.57 kB
text/typescript
import * as Lint from 'tslint'
import * as ts from 'typescript'
export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = {
open: 'Opening curly brace appears on the same line as controlling statement.',
body: 'Statement inside of curly braces should be on next line.',
close: 'Closing curly brace should be on the same line as opening curly ' +
'brace or on the line after the previous block.',
}
public apply(sourceFile: ts.SourceFile): Array<Lint.RuleFailure> {
const walker = new BraceStyleWalker(sourceFile, this.getOptions())
return this.applyWithWalker(walker)
}
}
class BraceStyleWalker extends Lint.RuleWalker {
private allowSingleLine: boolean = false
private srcText: string
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options)
this.srcText = sourceFile.getFullText()
this.allowSingleLine = this.getOptions()[1] && this.getOptions()[1].allowSingleLine
}
/* tslint:disable:cyclomatic-complexity */
protected visitBlock(block: ts.Block): void {
super.visitBlock(block)
const parent = block.parent as ts.Node
const { kind: parentKind } = parent
if (this.allowSingleLine &&
getStartPosition(block).line === getEndPosition(block).line)
{
return
}
const isFunction =
parentKind === ts.SyntaxKind.Constructor ||
parentKind === ts.SyntaxKind.MethodDeclaration ||
parentKind === ts.SyntaxKind.FunctionExpression ||
parentKind === ts.SyntaxKind.FunctionDeclaration
if (parentKind === ts.SyntaxKind.ArrowFunction ||
parentKind === ts.SyntaxKind.ElseKeyword ||
parentKind === ts.SyntaxKind.ForStatement ||
parentKind === ts.SyntaxKind.ForInStatement ||
parentKind === ts.SyntaxKind.ForOfStatement ||
parentKind === ts.SyntaxKind.TryStatement ||
parentKind === ts.SyntaxKind.CatchKeyword ||
parentKind === ts.SyntaxKind.CatchClause)
return
if (isFunction && parametersAreOnSameLine(block))
return
const blockChildren = block.getChildren()
const openingCurlyBrace =
blockChildren.filter((ch) => ch.kind === ts.SyntaxKind.OpenBraceToken).shift()
const closingCurlyBrace =
blockChildren.filter((ch) => ch.kind === ts.SyntaxKind.CloseBraceToken).pop()
const syntaxList =
blockChildren.filter((ch) => ch.kind === ts.SyntaxKind.SyntaxList).shift()
const blockPreviousNode =
parent.getChildren()[parent.getChildren().indexOf(block) - 1]
if (!openingCurlyBrace || !closingCurlyBrace || !syntaxList || !blockPreviousNode)
return
if (parentKind === ts.SyntaxKind.IfStatement) {
const children = parent.getChildren()
const previousNode = children[children.indexOf(block) - 1]
if (previousNode.kind === ts.SyntaxKind.ElseKeyword)
return
const openingParenthese =
children.filter((ch) => ch.kind === ts.SyntaxKind.OpenParenToken).shift()
const closingParenthese =
children.filter((ch) => ch.kind === ts.SyntaxKind.CloseParenToken).shift()
if (!openingParenthese || !closingParenthese)
return
const parenthesesSameLine = areOnSameLine(openingParenthese, closingParenthese)
if (parenthesesSameLine && areOnSameLine(closingParenthese, openingCurlyBrace))
return
}
if (parentKind === ts.SyntaxKind.WhileStatement) {
const children = parent.getChildren()
const openingParenthese =
children.filter((ch) => ch.kind === ts.SyntaxKind.OpenParenToken).shift()
const closingParenthese =
children.filter((ch) => ch.kind === ts.SyntaxKind.CloseParenToken).shift()
if (!openingParenthese || !closingParenthese)
return
const parenthesesSameLine = areOnSameLine(openingParenthese, closingParenthese)
if (parenthesesSameLine && areOnSameLine(closingParenthese, openingCurlyBrace))
return
}
const openingBracketError = areOnSameLine(blockPreviousNode, block)
if (openingBracketError) {
const nextNode = blockChildren[blockChildren.indexOf(openingCurlyBrace) + 1]
const indent = this.getNodeIndent(nextNode)
this.addFailure(this.createFailure(
openingCurlyBrace.getStart(),
openingCurlyBrace.getWidth(),
Rule.FAILURE_STRING.open,
))
}
if (syntaxList.getChildCount() > 0) {
const bodyError = areOnSameLine(openingCurlyBrace, syntaxList)
if (bodyError) {
this.addFailure(this.createFailure(
syntaxList.getStart(), syntaxList.getWidth(), Rule.FAILURE_STRING.body))
}
const nodeBeforeClosingBracket = syntaxList.getChildren()[syntaxList.getChildren().length - 1]
const closingBracketError = areOnSameLine(nodeBeforeClosingBracket, closingCurlyBrace)
if (closingBracketError) {
this.addFailure(this.createFailure(
closingCurlyBrace.getStart(), closingCurlyBrace.getWidth(), Rule.FAILURE_STRING.close))
}
}
}
private getNodeIndent(node: ts.Node): number {
if (node === this.getSourceFile()) {
return 0
}
if (node.kind === ts.SyntaxKind.SyntaxList) {
return this.getNodeIndent(node.parent as ts.Node)
}
const endIndex = node.getStart()
let pos = endIndex - 1
while (pos > 0 && this.srcText.charAt(pos) !== '\n') {
pos -= 1
}
const str = this.getSourceSubstr(pos + 1, endIndex)
const whiteSpace = (str.match(/^\s+/) || [ '' ])[0]
const indentChars = whiteSpace.split('')
const spaces = indentChars.filter((char) => char === ' ').length
return spaces
}
private getSourceSubstr(start: number, end: number) {
return this.srcText.substr(start, end - start)
}
}
function makeIndentation(num: number): string {
let str = ''
for (let i = 0; i < num - 2; ++i)
str += ' '
return str
}
function areOnSameLine(node: ts.Node, nextNode: ts.Node): boolean {
return getEndPosition(node).line === getStartPosition(nextNode).line
}
function getStartPosition(node: ts.Node) {
return node.getSourceFile().getLineAndCharacterOfPosition(node.getStart())
}
function getEndPosition(node: ts.Node) {
return node.getSourceFile().getLineAndCharacterOfPosition(node.getEnd())
}
function parametersAreOnSameLine(
block: ts.Block): boolean
{
const parent = block.parent as ts.FunctionDeclaration
const lastParameter = parent.parameters[parent.parameters.length - 1]
if (!lastParameter || areOnSameLine(lastParameter, parent))
return true
return false
}