import type { TSESTree , TSESLint } from '@typescript-eslint/experimental-utils' ; import { isArrowFunctionExpression , isIfStatement , isLogicalExpression } from '../utils/nodes' ; import { getMainFunctionTokenLocation , getFirstToken , getFirstTokenAfter , report , IssueLocation , issueLocation , } from '../utils/locations' ; import docsUrl from '../utils/docs-url' ; const DEFAULT_THRESHOLD = 15 ; type LoopStatement = TSESTree . ForStatement | TSESTree . ForInStatement | TSESTree . ForOfStatement | TSESTree . DoWhileStatement | TSESTree . WhileStatement ; type OptionalLocation = TSESTree . SourceLocation | null | undefined ; const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.' ; const rule = { meta : { messages : { refactorFunction : message , sonarRuntime : '{{sonarRuntimeData}}' , fileComplexity : '{{complexityAmount}}' , } , type : 'suggestion' , docs : { description : 'Cognitive Complexity of functions should not be too high' , recommended : 'error' , url : docsUrl ( __filename ) , } , schema : [ { type : 'integer' , minimum : 0 } , { enum : [ 'sonar-runtime' , 'metric' ] , } , ] , } , create ( context ) { const threshold = typeof context . options [ 0 ] === 'number' ? context . options [ 0 ] : DEFAULT_THRESHOLD ; const isFileComplexity = context . options . includes ( 'metric' ) ; /** Complexity of the file */ let fileComplexity = 0 ; /** Complexity of the current function if it is *not* considered nested to the first level function */ let complexityIfNotNested = [ ] ; /** Complexity of the current function if it is considered nested to the first level function */ let complexityIfNested = [ ] ; /** Current nesting level (number of enclosing control flow statements and functions) */ let nesting = 0 ; /** Indicator if the current top level function has a structural (generated by control flow statements) complexity */ let topLevelHasStructuralComplexity = false ; /** Indicator if the current top level function is React functional component */ const reactFunctionalComponent = { nameStartsWithCapital : false , returnsJsx : false , isConfirmed ( ) { return this . nameStartsWithCapital && this . returnsJsx ; } , init ( node ) { this . nameStartsWithCapital = nameStartsWithCapital ( node ) ; this . returnsJsx = false ; } , } ; /** Own (not including nested functions) complexity of the current top function */ let topLevelOwnComplexity = [ ] ; /** Nodes that should increase nesting level  */ const nestingNodes = new Set ( ) ; /** Set of already considered (with already computed complexity) logical expressions */ const consideredLogicalExpressions = new Set ( ) ; /** Stack of enclosing functions */ const enclosingFunctions = [ ] ; let secondLevelFunctions = [ ] ; return { ':function' : ( node ) => { onEnterFunction ( node as TSESTree . FunctionLike ) ; } , ':function:exit' ( node ) { onLeaveFunction ( node as TSESTree . FunctionLike ) ; } , '*' ( node ) { if ( nestingNodes . has ( node ) ) { nesting ++ ; } } , '*:exit' ( node ) { if ( nestingNodes . has ( node ) ) { nesting -- ; nestingNodes . delete ( node ) ; } } , Program ( ) { fileComplexity = 0 ; } , 'Program:exit' ( node ) { if ( isFileComplexity ) { context . report ( { node , messageId : 'fileComplexity' , data : { complexityAmount : fileComplexity } , } ) ; } } , IfStatement ( node ) { visitIfStatement ( node as TSESTree . IfStatement ) ; } , ForStatement ( node ) { visitLoop ( node as TSESTree . ForStatement ) ; } , ForInStatement ( node ) { visitLoop ( node as TSESTree . ForInStatement ) ; } , ForOfStatement ( node ) { visitLoop ( node as TSESTree . ForOfStatement ) ; } , DoWhileStatement ( node ) { visitLoop ( node as TSESTree . DoWhileStatement ) ; } , WhileStatement ( node ) { visitLoop ( node as TSESTree . WhileStatement ) ; } , SwitchStatement ( node ) { visitSwitchStatement ( node as TSESTree . SwitchStatement ) ; } , ContinueStatement ( node ) { visitContinueOrBreakStatement ( node as TSESTree . ContinueStatement ) ; } , BreakStatement ( node ) { visitContinueOrBreakStatement ( node as TSESTree . BreakStatement ) ; } , CatchClause ( node ) { visitCatchClause ( node as TSESTree . CatchClause ) ; } , LogicalExpression ( node ) { visitLogicalExpression ( node as TSESTree . LogicalExpression ) ; } , ConditionalExpression ( node ) { visitConditionalExpression ( node as TSESTree . ConditionalExpression ) ; } , ReturnStatement ( node ) { visitReturnStatement ( node as TSESTree . ReturnStatement ) ; } , } ; function onEnterFunction ( node ) { if ( enclosingFunctions . length === 0 ) { topLevelHasStructuralComplexity = false ; reactFunctionalComponent . init ( node ) ; topLevelOwnComplexity = [ ] ; secondLevelFunctions = [ ] ; } else if ( enclosingFunctions . length === 1 ) { complexityIfNotNested = [ ] ; complexityIfNested = [ ] ; } else { nesting ++ ; nestingNodes . add ( node ) ; } enclosingFunctions . push ( node ) ; } function onLeaveFunction ( node ) { enclosingFunctions . pop ( ) ; if ( enclosingFunctions . length === 0 ) { if ( topLevelHasStructuralComplexity && ! reactFunctionalComponent . isConfirmed ( ) ) { let totalComplexity = topLevelOwnComplexity ; secondLevelFunctions . forEach ( secondLevelFunction => { totalComplexity = totalComplexity . concat ( secondLevelFunction . complexityIfNested ) ; } ) ; checkFunction ( totalComplexity , getMainFunctionTokenLocation ( node , node . parent , context ) ) ; } else { checkFunction ( topLevelOwnComplexity , getMainFunctionTokenLocation ( node , node . parent , context ) ) ; secondLevelFunctions . forEach ( secondLevelFunction => { checkFunction ( secondLevelFunction . complexityIfThisSecondaryIsTopLevel , getMainFunctionTokenLocation ( secondLevelFunction . node , secondLevelFunction . parent , context ) ) ; } ) ; } } else if ( enclosingFunctions . length === 1 ) { secondLevelFunctions . push ( { node , parent : node . parent , complexityIfNested , complexityIfThisSecondaryIsTopLevel : complexityIfNotNested , loc : getMainFunctionTokenLocation ( node , node . parent , context ) , } ) ; } else { } } function visitIfStatement ( ifStatement ) { const { parent } = ifStatement ; const { loc : ifLoc } = getFirstToken ( ifStatement , context ) ; if ( isIfStatement ( parent ) && parent . alternate === ifStatement ) { addComplexity ( ifLoc ) ; } else { addStructuralComplexity ( ifLoc ) ; } nestingNodes . add ( ifStatement . consequent ) ; if ( ifStatement . alternate && ! isIfStatement ( ifStatement . alternate ) ) { nestingNodes . add ( ifStatement . alternate ) ; const elseTokenLoc = getFirstTokenAfter ( ifStatement . consequent , context ) ! . loc ; addComplexity ( elseTokenLoc ) ; } } function visitLoop ( loop ) { addStructuralComplexity ( getFirstToken ( loop , context ) . loc ) ; nestingNodes . add ( loop . body ) ; } function visitSwitchStatement ( switchStatement ) { addStructuralComplexity ( getFirstToken ( switchStatement , context ) . loc ) ; for ( const switchCase of switchStatement . cases ) { nestingNodes . add ( switchCase ) ; } } function visitContinueOrBreakStatement ( statement ) { if ( statement . label ) { addComplexity ( getFirstToken ( statement , context ) . loc ) ; } } function visitCatchClause ( catchClause ) { addStructuralComplexity ( getFirstToken ( catchClause , context ) . loc ) ; nestingNodes . add ( catchClause . body ) ; } function visitConditionalExpression ( conditionalExpression ) { const questionTokenLoc = getFirstTokenAfter ( conditionalExpression . test , context ) ! . loc ; addStructuralComplexity ( questionTokenLoc ) ; nestingNodes . add ( conditionalExpression . consequent ) ; nestingNodes . add ( conditionalExpression . alternate ) ; } function visitReturnStatement ( { argument } ) { if ( enclosingFunctions . length === 1 && argument && [ 'JSXElement' , 'JSXFragment' ] . includes ( argument . type as any ) ) { reactFunctionalComponent . returnsJsx = true ; } } function nameStartsWithCapital ( node ) { const checkFirstLetter = ( name ) => { const firstLetter = name [ 0 ] ; return firstLetter === firstLetter . toUpperCase ( ) ; } ; if ( ! isArrowFunctionExpression ( node ) && node . id ) { return checkFirstLetter ( node . id . name ) ; } const { parent } = node ; if ( parent && parent . type === 'VariableDeclarator' && parent . id . type === 'Identifier' ) { return checkFirstLetter ( parent . id . name ) ; } return false ; } function visitLogicalExpression ( logicalExpression ) { if ( ! consideredLogicalExpressions . has ( logicalExpression ) ) { const flattenedLogicalExpressions = flattenLogicalExpression ( logicalExpression ) ; let previous ; for ( const current of flattenedLogicalExpressions ) { if ( ! previous || previous . operator !== current . operator ) { const operatorTokenLoc = getFirstTokenAfter ( logicalExpression . left , context ) ! . loc ; addComplexity ( operatorTokenLoc ) ; } previous = current ; } } } function flattenLogicalExpression ( node ) { if ( isLogicalExpression ( node ) ) { consideredLogicalExpressions . add ( node ) ; return [ ... flattenLogicalExpression ( node . left ) , node , ... flattenLogicalExpression ( node . right ) , ] ; } return [ ] ; } function addStructuralComplexity ( location ) { const added = nesting + 1 ; const complexityPoint = { complexity : added , location } ; if ( enclosingFunctions . length === 0 ) { fileComplexity += added ; } else if ( enclosingFunctions . length === 1 ) { topLevelHasStructuralComplexity = true ; topLevelOwnComplexity . push ( complexityPoint ) ; } else { complexityIfNested . push ( { complexity : added + 1 , location } ) ; complexityIfNotNested . push ( complexityPoint ) ; } } function addComplexity ( location ) { const complexityPoint = { complexity : 1 , location } ; if ( enclosingFunctions . length === 0 ) { fileComplexity += 1 ; } else if ( enclosingFunctions . length === 1 ) { topLevelOwnComplexity . push ( complexityPoint ) ; } else { complexityIfNested . push ( complexityPoint ) ; complexityIfNotNested . push ( complexityPoint ) ; } } function checkFunction ( complexity = [ ] , loc ) { const complexityAmount = complexity . reduce ( ( acc , cur ) => acc + cur . complexity , 0 ) ; fileComplexity += complexityAmount ; if ( isFileComplexity ) { return ; } if ( complexityAmount > threshold ) { const secondaryLocations = complexity . map ( complexityPoint => { const { complexity , location } = complexityPoint ; const message = complexity === 1 ? '+1' : `+${ complexity } (incl. ${ complexity - 1 } for nesting)` ; return issueLocation ( location , undefined , message ) ; } ) ; report ( context , { messageId : 'refactorFunction' , data : { complexityAmount , threshold , } , loc , } , secondaryLocations , message , complexityAmount - threshold ) ; } } } , } ; export = rule ; type ComplexityPoint = { complexity : number ; location : TSESTree . SourceLocation ; } ;