import type { TSESLint , TSESTree } from '@typescript-eslint/experimental-utils' ; import { isIdentifier , findFirstMatchingAncestor , isReferenceTo , collectionConstructor , ancestorsChain , } from '../utils' ; import docsUrl from '../utils/docs-url' ; const nonAdditiveMutatorMethods = [ 'copyWithin' , 'pop' , 'reverse' , 'shift' , 'sort' , 'clear' , 'delete' , ] ; const accessorMethods = [ 'concat' , 'flat' , 'flatMap' , 'includes' , 'indexOf' , 'join' , 'lastIndexOf' , 'slice' , 'toSource' , 'toString' , 'toLocaleString' , 'get' , 'has' , ] ; const iterationMethods = [ 'entries' , 'every' , 'filter' , 'find' , 'findIndex' , 'forEach' , 'keys' , 'map' , 'reduce' , 'reduceRight' , 'some' , 'values' , ] ; const strictlyReadingMethods = new Set ( [ ... nonAdditiveMutatorMethods , ... accessorMethods , ... iterationMethods , ] ) ; const rule = { meta : { messages : { reviewUsageOfIdentifier : 'Review this usage of "{{identifierName}}" as it can only be empty here.' , } , schema : [ ] , type : 'problem' , docs : { description : 'Empty collections should not be accessed or iterated' , recommended : 'error' , url : docsUrl ( __filename ) , } , } , create ( context ) { return { 'Program:exit' : ( ) => { reportEmptyCollectionsUsage ( context . getScope ( ) , context ) ; } , } ; } , } ; function reportEmptyCollectionsUsage ( scope , context ) { if ( scope . type !== 'global' ) { scope . variables . forEach ( v => { reportEmptyCollectionUsage ( v , context ) ; } ) ; } scope . childScopes . forEach ( childScope => { reportEmptyCollectionsUsage ( childScope , context ) ; } ) ; } function reportEmptyCollectionUsage ( variable , context ) { if ( variable . references . length <= 1 ) { return ; } if ( variable . defs . some ( d => d . type === 'Parameter' || d . type === 'ImportBinding' ) ) { return ; } const readingUsages = [ ] ; let hasAssignmentOfEmptyCollection = false ; for ( const ref of variable . references ) { if ( ref . isWriteOnly ( ) ) { if ( isReferenceAssigningEmptyCollection ( ref ) ) { hasAssignmentOfEmptyCollection = true ; } else { return ; } } else if ( isReadingCollectionUsage ( ref ) ) { readingUsages . push ( ref ) ; } else { return ; } } if ( hasAssignmentOfEmptyCollection ) { readingUsages . forEach ( ref => { context . report ( { messageId : 'reviewUsageOfIdentifier' , data : { identifierName : ref . identifier . name , } , node : ref . identifier , } ) ; } ) ; } } function isReferenceAssigningEmptyCollection ( ref ) { const declOrExprStmt = findFirstMatchingAncestor ( ref . identifier as TSESTree . Node , n => n . type === 'VariableDeclarator' || n . type === 'ExpressionStatement' ) as TSESTree . Node ; if ( declOrExprStmt ) { if ( declOrExprStmt . type === 'VariableDeclarator' && declOrExprStmt . init ) { return isEmptyCollectionType ( declOrExprStmt . init ) ; } if ( declOrExprStmt . type === 'ExpressionStatement' ) { const { expression } = declOrExprStmt ; return ( expression . type === 'AssignmentExpression' && isReferenceTo ( ref , expression . left ) && isEmptyCollectionType ( expression . right ) ) ; } } return false ; } function isEmptyCollectionType ( node ) { if ( node && node . type === 'ArrayExpression' ) { return node . elements . length === 0 ; } else if ( node && ( node . type === 'CallExpression' || node . type === 'NewExpression' ) ) { return isIdentifier ( node . callee , ... collectionConstructor ) && node . arguments . length === 0 ; } return false ; } function isReadingCollectionUsage ( ref ) { return isStrictlyReadingMethodCall ( ref ) || isForIterationPattern ( ref ) || isElementRead ( ref ) ; } function isStrictlyReadingMethodCall ( usage ) { const { parent } = usage . identifier as TSESTree . Node ; if ( parent && parent . type === 'MemberExpression' ) { const memberExpressionParent = parent . parent ; if ( memberExpressionParent && memberExpressionParent . type === 'CallExpression' ) { return isIdentifier ( parent . property as TSESTree . Node , ... strictlyReadingMethods ) ; } } return false ; } function isForIterationPattern ( ref ) { const forInOrOfStatement = findFirstMatchingAncestor ( ref . identifier as TSESTree . Node , n => n . type === 'ForOfStatement' || n . type === 'ForInStatement' ) as TSESTree . ForOfStatement | TSESTree . ForInStatement ; return forInOrOfStatement && forInOrOfStatement . right === ref . identifier ; } function isElementRead ( ref ) { const { parent } = ref . identifier as TSESTree . Node ; return parent && parent . type === 'MemberExpression' && parent . computed && ! isElementWrite ( parent ) ; } function isElementWrite ( memberExpression ) { const ancestors = ancestorsChain ( memberExpression , new Set ( ) ) ; const assignment = ancestors . find ( n => n . type === 'AssignmentExpression' ) as TSESTree . AssignmentExpression ; if ( assignment && assignment . operator === '=' ) { return [ memberExpression , ... ancestors ] . includes ( assignment . left ) ; } return false ; } export = rule ;