license import * as Lint from "tslint" ; import { isJsxAttribute , isJsxElement , isJsxExpression , isJsxText , isTextualLiteral } from "tsutils/typeguard/3.0" ; import * as ts from "typescript" ; interface IOptions { allowPunctuation : boolean ; allowHtmlEntities : boolean ; } const htmlEntityRegex = /(&(?:#[0-9]+|[a-zA-Z]+);)/ ; export class Rule extends Lint . Rules . AbstractRule { public static metadata : Lint . IRuleMetadata = { ruleName : "jsx-use-translation-function" , description : Lint . Utils . dedent `
            Enforces use of a translation function. Most plain string literals are disallowed in JSX when enabled.` , options : { type : "array" , items : { type : "string" , enum : [ "allow-punctuation" , "allow-htmlentities" ] , } , } , optionsDescription : Lint . Utils . dedent `
            Whether to allow punctuation and or HTML entities` , type : "functionality" , typescriptOnly : false , } ; public static TRANSLATABLE_ATTRIBUTES = new Set ( [ "placeholder" , "title" , "alt" ] ) ; public static FAILURE_STRING = "String literals are disallowed as JSX. Use a translation function" ; public static FAILURE_STRING_FACTORY = ( text ) => `String literal is not allowed for value of ${ text }. Use a translation function` ; public apply ( sourceFile ) { return this . applyWithFunction ( sourceFile , walk , { allowHtmlEntities : this . ruleArguments . indexOf ( "allow-htmlentities" ) !== - 1 , allowPunctuation : this . ruleArguments . indexOf ( "allow-punctuation" ) !== - 1 , } ) ; } } function walk ( ctx ) { return ts . forEachChild ( ctx . sourceFile , function cb ( node ) : void { if ( isJsxElement ( node ) ) { for ( const child of node . children ) { if ( isJsxText ( child ) && isInvalidText ( child . getText ( ) , ctx . options ) ) { ctx . addFailureAtNode ( child , Rule . FAILURE_STRING ) ; } if ( isJsxExpression ( child ) && child . expression !== undefined && isTextualLiteral ( child . expression ) ) { if ( isInvalidText ( child . expression . text , ctx . options ) ) { ctx . addFailureAtNode ( child , Rule . FAILURE_STRING ) ; } } } } else if ( isJsxAttribute ( node ) ) { if ( Rule . TRANSLATABLE_ATTRIBUTES . has ( node . name . text ) && node . initializer !== undefined ) { if ( isTextualLiteral ( node . initializer ) && isInvalidText ( node . initializer . text , ctx . options ) ) { ctx . addFailureAtNode ( node . initializer , Rule . FAILURE_STRING_FACTORY ( node . name . text ) ) ; } if ( isJsxExpression ( node . initializer ) && isTextualLiteral ( node . initializer . expression ! ) ) { if ( isInvalidText ( ( node . initializer . expression as ts . LiteralExpression ) . text , ctx . options ) ) { ctx . addFailureAtNode ( node . initializer , Rule . FAILURE_STRING_FACTORY ( node . name . text ) ) ; } } } } return ts . forEachChild ( node , cb ) ; } ) ; } function isInvalidText ( text , options ) { const t = text . trim ( ) ; if ( t === "" ) { return false ; } if ( options . allowPunctuation && t . indexOf ( "&" ) === - 1 ) { return /\w/ . test ( t ) ; } const parts = t . split ( htmlEntityRegex ) . filter ( ( entity ) => entity !== "" ) ; return parts . some ( ( entity ) => { if ( options . allowHtmlEntities && htmlEntityRegex . test ( entity ) ) { return false ; } if ( options . allowPunctuation ) { return /\w/ . test ( entity ) ; } return true ; } ) ; }