import * as ts from 'typescript' ; import * as Lint from 'tslint' ; import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker' ; import { ExtendedMetadata } from './utils/ExtendedMetadata' ; import { AstUtils } from './utils/AstUtils' ; /**
 * Implementation of the no-feature-envy rule.
 */ export class Rule extends Lint . Rules . AbstractRule { public static metadata : ExtendedMetadata = { ruleName : 'no-feature-envy' , type : 'maintainability' , description : 'A method accesses the data of another object more than its own data.' , options : null , optionsDescription : '' , optionExamples : [ ] , recommendation : '[true, 1, ["_"]],' , typescriptOnly : false , issueClass : 'Non-SDL' , issueType : 'Warning' , severity : 'Moderate' , level : 'Opportunity for Excellence' , group : 'Clarity' , commonWeaknessEnumeration : '' , } ; public static FAILURE_STRING ( feature ) { const { methodName , className , otherClassName } = feature ; const failureMessage = `Method "${ methodName }" uses "${ otherClassName }" more than its own class "${ className }".` ; const recommendation = `Extract or Move Method from "${ methodName }" into "${ otherClassName }".` ; return `${ failureMessage } ${ recommendation }` ; } public apply ( sourceFile ) { return this . applyWithWalker ( new NoFeatureEnvyRuleWalker ( sourceFile , this . getOptions ( ) ) ) ; } } class NoFeatureEnvyRuleWalker extends ErrorTolerantWalker { private threshold : number = 0 ; private exclude : string [ ] = [ ] ; constructor ( sourceFile , options ) { super ( sourceFile , options ) ; this . parseOptions ( ) ; } protected visitClassDeclaration ( node ) { this . checkAndReport ( node ) ; super . visitClassDeclaration ( node ) ; } private checkAndReport ( node ) { this . getFeatureMethodsForClass ( node ) . forEach ( feature => { const failureMessage = Rule . FAILURE_STRING ( feature ) ; this . addFailureAtNode ( feature . methodNode , failureMessage ) ; } ) ; } private getFeatureMethodsForClass ( classNode ) { const methods = this . methodsForClass ( classNode ) ; return < any [ ] > methods . map ( method => { const walker = new ClassMethodWalker ( classNode , method ) ; return walker . features ( ) ; } ) . map ( features => this . getTopFeature ( features ) ) . filter ( feature => feature !== undefined ) ; } private getTopFeature ( features ) { const filteredFeatures = this . filterFeatures ( features ) ; return filteredFeatures . reduce ( ( best , current ) => { if ( ! best ) { return current ; } if ( current . featureEnvy ( ) > best . featureEnvy ( ) ) { return current ; } return best ; } , undefined ) ; } private filterFeatures ( features ) { return features . filter ( feature => { const isExcluded = this . exclude . indexOf ( feature . otherClassName ) !== - 1 ; if ( isExcluded ) { return false ; } return feature . featureEnvy ( ) > this . threshold ; } ) ; } protected methodsForClass ( classNode ) { return < ts . MethodDeclaration [ ] > classNode . members . filter ( ( classElement ) : boolean => { switch ( classElement . kind ) { case ts . SyntaxKind . MethodDeclaration : case ts . SyntaxKind . GetAccessor : case ts . SyntaxKind . SetAccessor : return ! AstUtils . isStatic ( classElement ) ; default : return false ; } } ) ; } private parseOptions ( ) { this . getOptions ( ) . forEach ( ( opt ) => { if ( typeof opt === 'boolean' ) { return ; } if ( typeof opt === 'number' ) { this . threshold = opt ; return ; } if ( Array . isArray ( opt ) ) { this . exclude = opt ; return ; } } ) ; } } class ClassMethodWalker extends Lint . SyntaxWalker { private featureEnvyMap : EnvyMap = { } ; constructor ( private classNode , private methodNode ) { super ( ) ; this . walk ( this . methodNode ) ; } public features ( ) { const thisClassAccesses = this . getCountForClass ( 'this' ) ; return this . classesUsed . map ( className => { const otherClassAccesses = this . getCountForClass ( className ) ; return new MethodFeature ( { classNode : this . classNode , methodNode : this . methodNode , otherClassName : className , thisClassAccesses , otherClassAccesses , } ) ; } ) ; } private getCountForClass ( className ) { return this . featureEnvyMap [ className ] || 0 ; } private get classesUsed ( ) : string [ ] { return Object . keys ( this . featureEnvyMap ) . filter ( className => className !== 'this' ) ; } protected visitPropertyAccessExpression ( node ) { if ( this . isTopPropertyAccess ( node ) ) { const className = this . classNameForPropertyAccess ( node ) ; this . incrementCountForClass ( className ) ; } super . visitPropertyAccessExpression ( node ) ; } private incrementCountForClass ( className ) { if ( this . featureEnvyMap [ className ] !== undefined ) { this . featureEnvyMap [ className ] += 1 ; } else { this . featureEnvyMap [ className ] = 1 ; } } private isTopPropertyAccess ( node ) { switch ( node . expression . kind ) { case ts . SyntaxKind . Identifier : case ts . SyntaxKind . ThisKeyword : case ts . SyntaxKind . SuperKeyword : return true ; } return false ; } private classNameForPropertyAccess ( node ) { const { expression } = node ; if ( ts . isThisTypeNode ( node ) ) { return 'this' ; } if ( expression . kind === ts . SyntaxKind . SuperKeyword ) { return 'this' ; } if ( this . classNode . name . getText ( ) === expression . getText ( ) ) { return 'this' ; } return expression . getText ( ) ; } } export class MethodFeature { constructor ( private data ) { } public get className ( ) : string { return this . classNode . name . text ; } public get classNode ( ) : ts . ClassDeclaration { return this . data . classNode ; } public get methodName ( ) : string { return this . methodNode . name . getText ( ) ; } public get methodNode ( ) : ts . MethodDeclaration { return this . data . methodNode ; } public featureEnvy ( ) { const { thisClassAccesses , otherClassAccesses } = this . data ; return otherClassAccesses - thisClassAccesses ; } public get otherClassName ( ) : string { return this . data . otherClassName ; } } interface EnvyMap { [ className ] : number ; }