import type { TSESTree , TSESLint } from '@typescript-eslint/experimental-utils' ; import docsUrl from '../utils/docs-url' ; import { issueLocation , report } from '../utils/locations' ; const DEFAULT_THRESHOLD = 3 ; const MIN_LENGTH = 10 ; const NO_SEPARATOR_REGEXP = /^\w*$/ ; const EXCLUDED_CONTEXTS = [ 'ImportDeclaration' , 'ImportExpression' , 'JSXAttribute' , 'ExportAllDeclaration' , 'ExportNamedDeclaration' , ] ; const message = 'Define a constant instead of duplicating this literal {{times}} times.' ; type Options = ( number | 'sonar-runtime' ) [ ] ; type Context = TSESLint . RuleContext < string , Options > ; const rule = { meta : { messages : { defineConstant : message , sonarRuntime : '{{sonarRuntimeData}}' , } , type : 'suggestion' , docs : { description : 'String literals should not be duplicated' , recommended : 'error' , url : docsUrl ( __filename ) , } , schema : [ { type : 'integer' , minimum : 2 } , { enum : [ 'sonar-runtime' ] } , ] , } , create ( context ) { const literalsByValue = new Map ( ) ; const threshold = typeof context . options [ 0 ] === 'number' ? context . options [ 0 ] : DEFAULT_THRESHOLD ; return { Literal : ( node ) => { const literal = node as TSESTree . Literal ; const { parent } = literal ; if ( typeof literal . value === 'string' && parent && ! [ 'ExpressionStatement' , 'TSLiteralType' ] . includes ( parent . type ) ) { const stringContent = literal . value . trim ( ) ; if ( ! isExcludedByUsageContext ( context , literal ) && stringContent . length >= MIN_LENGTH && ! stringContent . match ( NO_SEPARATOR_REGEXP ) ) { const sameStringLiterals = literalsByValue . get ( stringContent ) || [ ] ; sameStringLiterals . push ( literal ) ; literalsByValue . set ( stringContent , sameStringLiterals ) ; } } } , 'Program:exit' ( ) { literalsByValue . forEach ( literals => { if ( literals . length >= threshold ) { const [ primaryNode , ... secondaryNodes ] = literals ; const secondaryIssues = secondaryNodes . map ( node => issueLocation ( node . loc , node . loc , 'Duplication' ) ) ; report ( context , { messageId : 'defineConstant' , node : primaryNode , data : { times : literals . length . toString ( ) } , } , secondaryIssues , message ) ; } } ) ; } , } ; } , } ; function isExcludedByUsageContext ( context , literal ) { const parent = literal . parent ! ; const parentType = parent . type ; return ( EXCLUDED_CONTEXTS . includes ( parentType ) || isRequireContext ( parent , context ) || isObjectPropertyKey ( parent , literal ) ) ; } function isRequireContext ( parent , context ) { return ( parent . type === 'CallExpression' && context . getSourceCode ( ) . getText ( parent . callee ) === 'require' ) ; } function isObjectPropertyKey ( parent , literal ) { return parent . type === 'Property' && parent . key === literal ; } export = rule ;