import * as Lint from "tslint" ; import * as ts from "typescript" ; const OPTION_COMPONENT = "component" ; const OPTION_LITERAL = "literal" ; export class Rule extends Lint . Rules . AbstractRule { public static metadata : Lint . IRuleMetadata = { ruleName : "blueprint-icon-components" , description : "Enforce usage of JSX Icon components over IconName string literals (or vice-versa)" , options : { items : [ { enum : [ OPTION_COMPONENT , OPTION_LITERAL ] , type : "string" } ] , maxLength : 1 , minLength : 0 , type : "array" , } , optionsDescription : Lint . Utils . dedent `
            Accepts one option which can be either of the following values:
            * \`"${ OPTION_COMPONENT }"\` (default) requires JSX Icon components for \`icon\` props.
            * \`"${ OPTION_LITERAL }"\` requires \`IconName\` string literals for \`icon\` props.
            A fixer is available for \`"${ OPTION_COMPONENT }"\` that converts a string literal to the corresponding component.` , optionExamples : [ `true` , `false` , `[true, "${ OPTION_COMPONENT }"]` , `[true, "${ OPTION_LITERAL }"]` ] , type : "functionality" , typescriptOnly : false , } ; public static componentMessage = ( component ) => `Replace icon literal with component: ${ component }` ; public static literalMessage = ( literal ) => `Replace icon component with literal: ${ literal }` ; public apply ( sourceFile ) { const [ option = OPTION_COMPONENT ] = this . ruleArguments ; return this . applyWithFunction ( sourceFile , walk , option ) ; } } function walk ( ctx ) { return ts . forEachChild ( ctx . sourceFile , function cb ( node ) : void { if ( ts . isJsxAttribute ( node ) && node . name . text === "icon" ) { const { initializer } = node ; const option = ctx . options ; if ( initializer === undefined ) { } else if ( ts . isStringLiteral ( initializer ) && option === OPTION_COMPONENT ) { const iconName = `<${ pascalCase ( initializer . text ) }Icon />` ; addFailure ( ctx , node , Rule . componentMessage ( iconName ) , `{${ iconName }}` ) ; } else if ( ts . isJsxExpression ( initializer ) && option === OPTION_LITERAL ) { const match = /<(\w+)Icon / . exec ( initializer . getText ( ) ) ; if ( match != null ) { const message = Rule . literalMessage ( `"${ dashCase ( match [ 1 ] ) }"` ) ; addFailure ( ctx , node , message , message ) ; } } } return ts . forEachChild ( node , cb ) ; } ) ; } function addFailure ( ctx , node , message , replacement ? ) { const offsetLength = "icon=" . length ; const start = node . getStart ( ctx . sourceFile ) + offsetLength ; const width = node . getWidth ( ctx . sourceFile ) - offsetLength ; const fixer = replacement == null ? undefined : new Lint . Replacement ( start , width , replacement ) ; ctx . addFailureAt ( start , width , message , fixer ) ; } /** "MultiWordPhrase" => "multi-word-phrase" */ function dashCase ( text ) { return text . replace ( /[A-Z]/g , match => `-${ match . toLowerCase ( ) }` ) . replace ( /^-+/ , "" ) ; } /** "multi-word-phrase" => "MultiWordPhrase" */ function pascalCase ( text ) { return text . split ( "-" ) . map ( s => s [ 0 ] . toUpperCase ( ) + s . slice ( 1 ) . toLowerCase ( ) ) . join ( "" ) ; }