import type { TSESLint , TSESTree } from '@typescript-eslint/experimental-utils' ; import { collectionConstructor , writingMethods } from '../utils/collections' ; import docsUrl from '../utils/docs-url' ; const rule = { meta : { messages : { unusedCollection : "Either use this collection's contents or remove the collection." , } , schema : [ ] , type : 'problem' , docs : { description : 'Collection and array contents should be used' , recommended : 'error' , url : docsUrl ( __filename ) , } , } , create ( context ) { return { 'Program:exit' : ( ) => { const unusedArrays = [ ] ; collectUnusedCollections ( context . getScope ( ) , unusedArrays ) ; unusedArrays . forEach ( unusedArray => { context . report ( { messageId : 'unusedCollection' , node : unusedArray . identifiers [ 0 ] , } ) ; } ) ; } , } ; } , } ; function collectUnusedCollections ( scope , unusedArray ) { if ( scope . type !== 'global' ) { scope . variables . filter ( isUnusedCollection ) . forEach ( v => { unusedArray . push ( v ) ; } ) ; } scope . childScopes . forEach ( childScope => { collectUnusedCollections ( childScope , unusedArray ) ; } ) ; } function isExported ( variable ) { const definition = variable . defs [ 0 ] ; return definition && definition . node . parent ?. parent ?. type . startsWith ( 'Export' ) ; } function isUnusedCollection ( variable ) { if ( isExported ( variable ) ) { return false ; } if ( variable . references . length <= 1 ) { return false ; } let assignCollection = false ; for ( const ref of variable . references ) { if ( ref . isWriteOnly ( ) ) { if ( isReferenceAssigningCollection ( ref ) ) { assignCollection = true ; } else { return false ; } } else if ( isRead ( ref ) ) { return false ; } } return assignCollection ; } function isReferenceAssigningCollection ( 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 isCollectionType ( declOrExprStmt . init ) ; } if ( declOrExprStmt . type === 'ExpressionStatement' ) { const { expression } = declOrExprStmt ; return ( expression . type === 'AssignmentExpression' && isReferenceTo ( ref , expression . left ) && isCollectionType ( expression . right ) ) ; } } return false ; } function isCollectionType ( node ) { if ( node && node . type === 'ArrayExpression' ) { return true ; } else if ( node && ( node . type === 'CallExpression' || node . type === 'NewExpression' ) ) { return isIdentifier ( node . callee , ... collectionConstructor ) ; } return false ; } function isRead ( ref ) { const expressionStatement = findFirstMatchingAncestor ( ref . identifier as TSESTree . Node , n => n . type === 'ExpressionStatement' ) as TSESTree . ExpressionStatement ; if ( expressionStatement ) { return ! ( isElementWrite ( expressionStatement , ref ) || isWritingMethodCall ( expressionStatement , ref ) ) ; } return true ; } /**
 * Detect expression statements like the following:
 * myArray.push(1);
 */ function isWritingMethodCall ( statement , ref ) { if ( statement . expression . type === 'CallExpression' ) { const { callee } = statement . expression ; if ( isMemberExpression ( callee ) ) { const { property } = callee ; return isReferenceTo ( ref , callee . object ) && isIdentifier ( property , ... writingMethods ) ; } } return false ; } function isMemberExpression ( node ) { return node . type === 'MemberExpression' ; } /**
 * Detect expression statements like the following:
 *  myArray[1] = 42;
 *  myArray[1] += 42;
 *  myObj.prop1 = 3;
 *  myObj.prop1 += 3;
 */ function isElementWrite ( statement , ref ) { if ( statement . expression . type === 'AssignmentExpression' ) { const assignmentExpression = statement . expression ; const lhs = assignmentExpression . left ; return isMemberExpressionReference ( lhs , ref ) ; } return false ; } function isMemberExpressionReference ( lhs , ref ) { return ( lhs . type === 'MemberExpression' && ( isReferenceTo ( ref , lhs . object ) || isMemberExpressionReference ( lhs . object , ref ) ) ) ; } function isIdentifier ( node , ... values ) { return node . type === 'Identifier' && values . some ( value => value === node . name ) ; } function isReferenceTo ( ref , node ) { return node . type === 'Identifier' && node === ref . identifier ; } function findFirstMatchingAncestor ( node , predicate ) { return ancestorsChain ( node , new Set ( ) ) . find ( predicate ) ; } function ancestorsChain ( node , boundaryTypes ) { const chain = [ ] ; let currentNode = node . parent ; while ( currentNode ) { chain . push ( currentNode ) ; if ( boundaryTypes . has ( currentNode . type ) ) { break ; } currentNode = currentNode . parent ; } return chain ; } export = rule ;