import type { TSESLint , TSESTree } from '@typescript-eslint/experimental-utils' ; import { report } from '../utils/locations' ; import { isIdentifier , isIfStatement } from '../utils/nodes' ; import docsUrl from '../utils/docs-url' ; const message = 'This always evaluates to {{value}}. Consider refactoring this code.' ; const rule = { meta : { messages : { refactorBooleanExpression : message , sonarRuntime : '{{sonarRuntimeData}}' , } , type : 'suggestion' , docs : { description : 'Boolean expressions should not be gratuitous' , recommended : 'error' , url : docsUrl ( __filename ) , } , schema : [ { enum : [ 'sonar-runtime' ] , } , ] , } , create ( context ) { const truthyMap = new Map ( ) ; const falsyMap = new Map ( ) ; return { IfStatement : ( node ) => { const { test } = node as TSESTree . IfStatement ; if ( test . type === 'Literal' && typeof test . value === 'boolean' ) { reportIssue ( test , undefined , context , test . value ) ; } } , ':statement' : ( node ) => { const { parent } = node ; if ( isIfStatement ( parent ) ) { const currentScope = context . getScope ( ) ; if ( parent . consequent === node ) { const { truthy , falsy } = collectKnownIdentifiers ( parent . test ) ; truthyMap . set ( parent . consequent , transformAndFilter ( truthy , currentScope ) ) ; falsyMap . set ( parent . consequent , transformAndFilter ( falsy , currentScope ) ) ; } else if ( parent . alternate === node && isIdentifier ( parent . test ) ) { falsyMap . set ( parent . alternate , transformAndFilter ( [ parent . test ] , currentScope ) ) ; } } } , ':statement:exit' : ( node ) => { const stmt = node as TSESTree . Statement ; truthyMap . delete ( stmt ) ; falsyMap . delete ( stmt ) ; } , Identifier : ( node ) => { const id = node as TSESTree . Identifier ; const symbol = getSymbol ( id , context . getScope ( ) ) ; const { parent } = node ; if ( ! symbol || ! parent ) { return ; } if ( ! isLogicalAnd ( parent ) && ! isLogicalOrLhs ( id , parent ) && ! isIfStatement ( parent ) && ! isLogicalNegation ( parent ) ) { return ; } const checkIfKnownAndReport = ( map , truthy ) => { map . forEach ( references => { const ref = references . find ( ref => ref . resolved === symbol ) ; if ( ref ) { reportIssue ( id , ref , context , truthy ) ; } } ) ; } ; checkIfKnownAndReport ( truthyMap , true ) ; checkIfKnownAndReport ( falsyMap , false ) ; } , Program : ( ) => { truthyMap . clear ( ) ; falsyMap . clear ( ) ; } , } ; } , } ; function collectKnownIdentifiers ( expression ) { const truthy = [ ] ; const falsy = [ ] ; const checkExpr = ( expr ) => { if ( isIdentifier ( expr ) ) { truthy . push ( expr ) ; } else if ( isLogicalNegation ( expr ) ) { if ( isIdentifier ( expr . argument ) ) { falsy . push ( expr . argument ) ; } else if ( isLogicalNegation ( expr . argument ) && isIdentifier ( expr . argument . argument ) ) { truthy . push ( expr . argument . argument ) ; } } } ; let current = expression ; checkExpr ( current ) ; while ( isLogicalAnd ( current ) ) { checkExpr ( current . right ) ; current = current . left ; } checkExpr ( current ) ; return { truthy , falsy } ; } function isLogicalAnd ( expression ) { return expression . type === 'LogicalExpression' && expression . operator === '&&' ; } function isLogicalOrLhs ( id , expression ) { return ( expression . type === 'LogicalExpression' && expression . operator === '||' && expression . left === id ) ; } function isLogicalNegation ( expression ) { return expression . type === 'UnaryExpression' && expression . operator === '!' ; } function isDefined < T > ( x ) { return x != null ; } function getSymbol ( id , scope ) { const ref = scope . references . find ( r => r . identifier === id ) ; if ( ref ) { return ref . resolved ; } return null ; } function getFunctionScope ( scope ) { if ( scope . type === 'function' ) { return scope ; } else if ( ! scope . upper ) { return null ; } return getFunctionScope ( scope . upper ) ; } function mightBeWritten ( symbol , currentScope ) { return symbol . references . filter ( ref => ref . isWrite ( ) ) . find ( ref => { const refScope = ref . from ; let cur = refScope ; while ( cur ) { if ( cur === currentScope ) { return true ; } cur = cur . upper ; } const currentFunc = getFunctionScope ( currentScope ) ; const refFunc = getFunctionScope ( refScope ) ; return refFunc !== currentFunc ; } ) ; } function transformAndFilter ( ids , currentScope ) { return ids . map ( id => currentScope . upper ?. references . find ( r => r . identifier === id ) ) . filter ( isDefined ) . filter ( ref => isDefined ( ref . resolved ) ) . filter ( ref => ! mightBeWritten ( ref . resolved ! , currentScope ) ) ; } function reportIssue ( id , ref , context , truthy ) { const value = truthy ? 'truthy' : 'falsy' ; report ( context , { messageId : 'refactorBooleanExpression' , data : { value , } , node : id , } , getSecondaryLocations ( ref , value ) , message ) ; } function getSecondaryLocations ( ref , truthy ) { if ( ref ) { const secLoc = ref . identifier . loc ! ; return [ { message : `Evaluated here to be ${ truthy }` , line : secLoc . start . line , column : secLoc . start . column , endLine : secLoc . end . line , endColumn : secLoc . end . column , } , ] ; } else { return [ ] ; } } export = rule ;