license import * as Lint from "tslint" ; import { isJsxElement , isJsxSelfClosingElement } from "tsutils/typeguard/3.0" ; import * as ts from "typescript" ; export class Rule extends Lint . Rules . AbstractRule { public static metadata : Lint . IRuleMetadata = { ruleName : "jsx-alignment" , description : "Enforces consistent and readable vertical alignment of JSX tags and attributes" , optionsDescription : "Not configurable." , options : null , optionExamples : [ "true" ] , type : "style" , typescriptOnly : false , } ; public static ATTR_LINE_FAILURE = "JSX attributes must be on a line below the opening tag" ; public static ATTR_INDENT_FAILURE = "JSX attributes must be indented further than the opening tag statement" ; public static ATTR_ALIGN_FAILURE = "JSX attributes must be on their own line and vertically aligned" ; public static TAG_CLOSE_FAILURE = "Tag closing must be on its own line and aligned with opening of tag" ; public static CLOSING_TAG_FAILURE = "Closing tag must be on its own line and aligned with opening tag" ; public apply ( sourceFile ) { return this . applyWithFunction ( sourceFile , walk ) ; } } const LEADING_WHITESPACE_REGEX = /[ \t]/ ; function walk ( ctx ) { return ts . forEachChild ( ctx . sourceFile , function cb ( node ) : void { if ( isJsxElement ( node ) ) { if ( isMultiline ( node . openingElement ) ) { const startLocation = getLineAndCharacter ( node ) ; const closeLocation = ctx . sourceFile . getLineAndCharacterOfPosition ( node . openingElement . getEnd ( ) - ">" . length ) ; checkElement ( startLocation , node . openingElement . attributes , closeLocation , node . closingElement ) ; } } else if ( isJsxSelfClosingElement ( node ) ) { if ( isMultiline ( node ) ) { const startLocation = getLineAndCharacter ( node ) ; const closeLocation = ctx . sourceFile . getLineAndCharacterOfPosition ( node . getEnd ( ) - "/>" . length ) ; checkElement ( startLocation , node . attributes , closeLocation ) ; } } return ts . forEachChild ( node , cb ) ; } ) ; function checkElement ( elementOpen , attributes , elementClose , closingTag ? ) { const attrs = Array . isArray ( attributes ) ? attributes : attributes . properties ; if ( attrs . length === 0 ) { return ; } const initialIndent = getFirstNonWhitespaceCharacter ( elementOpen . line ) ; const firstAttr = attrs [ 0 ] ; const firstAttrCharacter = getCharacter ( firstAttr ) ; if ( getLine ( firstAttr ) === elementOpen . line ) { reportFailure ( firstAttr , Rule . ATTR_LINE_FAILURE ) ; } let lastSeenLine = - 1 ; for ( const attr of attrs ) { const character = getCharacter ( attr ) ; if ( character <= initialIndent ) { reportFailure ( attr , Rule . ATTR_INDENT_FAILURE ) ; } if ( attr !== firstAttr && character !== firstAttrCharacter ) { reportFailure ( attr , Rule . ATTR_ALIGN_FAILURE ) ; } lastSeenLine = getLine ( attr ) ; } if ( lastSeenLine === elementClose . line || elementClose . character !== initialIndent ) { const start = ctx . sourceFile . getPositionOfLineAndCharacter ( elementClose . line , elementClose . character ) ; ctx . addFailureAt ( start , 1 , Rule . TAG_CLOSE_FAILURE ) ; } if ( closingTag !== undefined ) { const closingTagLocation = getLineAndCharacter ( closingTag ) ; if ( closingTagLocation . line <= elementClose . line || closingTagLocation . character !== initialIndent ) { reportFailure ( closingTag , Rule . CLOSING_TAG_FAILURE ) ; } } } function getFirstNonWhitespaceCharacter ( line ) { const lineStart = ctx . sourceFile . getLineStarts ( ) [ line ] ; const source = ctx . sourceFile . getFullText ( ) ; let width = 0 ; while ( lineStart + width < source . length && LEADING_WHITESPACE_REGEX . test ( source . charAt ( lineStart + width ) ) ) { width ++ ; } return width ; } function isMultiline ( node ) { const startLine = getLine ( node ) ; const endLine = ctx . sourceFile . getLineAndCharacterOfPosition ( node . getEnd ( ) ) . line ; return startLine !== endLine ; } function getLineAndCharacter ( node ) { return ctx . sourceFile . getLineAndCharacterOfPosition ( node . getStart ( ctx . sourceFile ) ) ; } function getCharacter ( node ) { return getLineAndCharacter ( node ) . character ; } function getLine ( node ) { return getLineAndCharacter ( node ) . line ; } function reportFailure ( node , message ) { ctx . addFailureAt ( node . getStart ( ) , node . getWidth ( ) , message ) ; } }