import * as path from 'path' ; import * as ts from 'typescript' ; import * as Lint from 'tslint' ; import * as minimatch from 'minimatch' ; export class Rule extends Lint . Rules . AbstractRule { apply ( sourceFile ) { return this . applyWithWalker ( new Walker ( sourceFile , this . getOptions ( ) ) ) ; } } /**
 * Token used to indicate that all properties of an object
 * should be linted against a single pattern.
 */ const ALL_PROPS_TOKEN = '*' ; /** Object that can be used to configured the rule. */ interface RuleConfig { [ key ] : { argument : number ; required ? : boolean ; properties : { [ key ] : string ; } ; } ; } /** Represents a set of required and forbidden decorator properties. */ type DecoratorRuleSet = { argument : number ; required : boolean ; requiredProps : { [ key ] : RegExp ; } ; forbiddenProps : { [ key ] : RegExp ; } ; } ; /** Represents a map between decorator names and rule sets. */ type DecoratorRules = { [ decorator ] : DecoratorRuleSet ; } ; class Walker extends Lint . RuleWalker { private _enabled : boolean ; private _rules : DecoratorRules ; constructor ( sourceFile , options ) { super ( sourceFile , options ) ; const fileGlobs = options . ruleArguments . slice ( 1 ) || [ ] ; const relativeFilePath = path . relative ( process . cwd ( ) , sourceFile . fileName ) ; this . _rules = this . _generateRules ( options . ruleArguments [ 0 ] ) ; this . _enabled = Object . keys ( this . _rules ) . length > 0 && fileGlobs . some ( p => minimatch ( relativeFilePath , p ) ) ; } visitClassDeclaration ( node ) { if ( this . _enabled ) { if ( node . decorators ) { node . decorators . forEach ( decorator => this . _validateDecorator ( decorator ) ) ; } node . members . forEach ( member => { if ( member . decorators ) { member . decorators . forEach ( decorator => this . _validateDecorator ( decorator ) ) ; } } ) ; } super . visitClassDeclaration ( node ) ; } param decorator private _validateDecorator ( decorator ) { const expression = decorator . expression ; if ( ! expression || ! ts . isCallExpression ( expression ) ) { return ; } const rules = this . _rules [ expression . expression . getText ( ) ] ; const args = expression . arguments ; if ( ! rules ) { return ; } const allPropsRequirement = rules . requiredProps [ ALL_PROPS_TOKEN ] ; if ( allPropsRequirement ) { const argumentText = args [ rules . argument ] ? args [ rules . argument ] . getText ( ) : '' ; if ( ! allPropsRequirement . test ( argumentText ) ) { this . addFailureAtNode ( expression . parent , `Expected decorator argument ${ rules . argument } ` + `to match "${ allPropsRequirement }"` ) ; } return ; } if ( ! args [ rules . argument ] ) { if ( rules . required ) { this . addFailureAtNode ( expression . parent , `Missing required argument at index ${ rules . argument }` ) ; } return ; } if ( ! ts . isObjectLiteralExpression ( args [ rules . argument ] ) ) { return ; } const props = [ ] ; ( args [ rules . argument ] as ts . ObjectLiteralExpression ) . properties . forEach ( prop => { if ( ts . isPropertyAssignment ( prop ) && prop . name && prop . initializer ) { props . push ( { name : prop . name . getText ( ) , value : prop . initializer . getText ( ) , node : prop } ) ; } } ) ; const missing = Object . keys ( rules . requiredProps ) . filter ( key => ! props . find ( prop => prop . name === key ) ) ; if ( missing . length ) { this . addFailureAtNode ( expression . expression , 'Missing required properties: ' + missing . join ( ', ' ) ) ; } else { props . filter ( prop => rules . requiredProps [ prop . name ] || rules . forbiddenProps [ prop . name ] ) . forEach ( prop => { const { name , value , node } = prop ; const requiredPattern = rules . requiredProps [ name ] ; const forbiddenPattern = rules . forbiddenProps [ name ] ; if ( requiredPattern && ! requiredPattern . test ( value ) ) { this . addFailureAtNode ( node , `Invalid value for property. ` + `Expected value to match "${ requiredPattern }".` ) ; } else if ( forbiddenPattern && forbiddenPattern . test ( value ) ) { this . addFailureAtNode ( node , `Property value not allowed. ` + `Value should not match "${ forbiddenPattern }".` ) ; } } ) ; } } param config returns private _generateRules ( config ) { const output = { } ; if ( config ) { Object . keys ( config ) . forEach ( decoratorName => { const decoratorConfig = config [ decoratorName ] ; const { argument , properties , required } = decoratorConfig ; const allProperties = properties [ ALL_PROPS_TOKEN ] ; if ( allProperties ) { output [ decoratorName ] = { argument , required : ! ! required , requiredProps : { [ ALL_PROPS_TOKEN ] : new RegExp ( allProperties ) } , forbiddenProps : { } } ; } else { output [ decoratorName ] = Object . keys ( decoratorConfig . properties ) . reduce ( ( rules , prop ) => { const isForbidden = prop . startsWith ( '!' ) ; const cleanName = isForbidden ? prop . slice ( 1 ) : prop ; const pattern = new RegExp ( properties [ prop ] ) ; if ( isForbidden ) { rules . forbiddenProps [ cleanName ] = pattern ; } else { rules . requiredProps [ cleanName ] = pattern ; } return rules ; } , { argument , required : ! ! required , requiredProps : { } as { [ key ] : RegExp ; } , forbiddenProps : { } as { [ key ] : RegExp ; } } ) ; } } ) ; } return output ; } }