import * as Lint from "tslint" ; import { isExpressionStatement , isJsxAttribute , isPropertyAssignment , isStringLiteral } from "tsutils/typeguard/2.8" ; import * as ts from "typescript" ; import { addImportToFile } from "./utils/addImportToFile" ; const BLUEPRINT_CLASSNAME_PATTERN = /[^\w-<.]((pt|bp3)-(?!icon-?)[\w-]+)/g ; export class Rule extends Lint . Rules . AbstractRule { public static metadata : Lint . IRuleMetadata = { ruleName : "blueprint-classes-constants" , description : "Enforce usage of Classes constants over namespaced string literals." , options : null , optionsDescription : "Not configurable" , optionExamples : [ "true" ] , type : "style" , typescriptOnly : false , } ; public static FAILURE_STRING = "use Blueprint `Classes` constant instead of string literal" ; public apply ( sourceFile ) { return this . applyWithFunction ( sourceFile , walk ) ; } } function walk ( ctx ) { let shouldFixImports = true ; return ts . forEachChild ( ctx . sourceFile , callback ) ; function callback ( node ) { if ( ts . isStringLiteralLike ( node ) || ts . isTemplateExpression ( node ) ) { const prefixMatches = getAllMatches ( node . getFullText ( ) ) ; if ( prefixMatches . length > 0 ) { const ptClassStrings = prefixMatches . map ( m => m . match ) ; const replacementText = isStringLiteral ( node ) ? wrapForParent ( getLiteralReplacement ( node . getText ( ) , ptClassStrings ) , node ) : getTemplateReplacement ( node . getText ( ) , ptClassStrings ) ; const replacements = [ new Lint . Replacement ( node . getStart ( ) , node . getWidth ( ) , replacementText ) ] ; if ( shouldFixImports ) { replacements . unshift ( addImportToFile ( ctx . sourceFile , [ "Classes" ] , "@blueprintjs/core" ) ) ; shouldFixImports = false ; } ctx . addFailureAt ( node . getFullStart ( ) + prefixMatches [ 0 ] . index + 1 , prefixMatches [ 0 ] . match . length , Rule . FAILURE_STRING , replacements ) ; } } return ts . forEachChild ( node , callback ) ; } } /** Returns array of all invalid string matches detected in the given className string. */ function getAllMatches ( className ) { const ptMatches = [ ] ; let currentMatch ; while ( ( currentMatch = BLUEPRINT_CLASSNAME_PATTERN . exec ( className ) ) != null ) { ptMatches . push ( { match : currentMatch [ 1 ] , index : currentMatch . index || 0 } ) ; } return ptMatches ; } /** Produce replacement text for a string literal that contains invalid classes. */ function getLiteralReplacement ( className , ptClassStrings ) { const stringWithoutPtClasses = ptClassStrings . reduce ( ( value , cssClass ) => value . replace ( cssClass , "" ) , className ) . slice ( 1 , - 1 ) . replace ( /(\s)+/ , "$1" ) . trim ( ) ; if ( stringWithoutPtClasses . length === 0 && ptClassStrings . length === 1 ) { return convertPtClassName ( ptClassStrings [ 0 ] ) ; } const templateStrings = ptClassStrings . map ( n => `\${${ convertPtClassName ( n ) }}` ) . join ( " " ) ; return `\`${ [ templateStrings , stringWithoutPtClasses ] . join ( " " ) . trim ( ) }\`` ; } /** Produce replacement text for a `template string` that contains invalid classes. */ function getTemplateReplacement ( className , ptClassStrings ) { return ptClassStrings . reduce ( ( value , cssClass ) => value . replace ( cssClass , `\${${ convertPtClassName ( cssClass ) }}` ) , className ) ; } /** Wrap the given statement based on the type of the parent node: JSX props, expressions, etc. */ function wrapForParent ( statement , node ) { const { parent } = node ; if ( parent === undefined ) { return statement ; } else if ( isJsxAttribute ( parent ) ) { return `{${ statement }}` ; } else if ( isExpressionStatement ( parent ) ) { return `[${ statement }]` ; } else if ( isPropertyAssignment ( parent ) && parent . getChildAt ( 0 ) === node ) { return `[${ statement }]` ; } else { return statement ; } } /** Converts a `pt-class-name` literal to `Classes.CLASS_NAME` constant. */ function convertPtClassName ( text ) { const className = text . replace ( /(pt|bp3)-/ , "" ) . replace ( /-/g , "_" ) . toUpperCase ( ) ; return `Classes.${ className }` ; }