UNPKG

8.33 kBPlain TextView Raw
1import * as ts from 'typescript';
2import * as Lint from 'tslint';
3
4import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker';
5import { ExtendedMetadata } from './utils/ExtendedMetadata';
6import { AstUtils } from './utils/AstUtils';
7
8/**
9 * Implementation of the no-feature-envy rule.
10 */
11export class Rule extends Lint.Rules.AbstractRule {
12 public static metadata: ExtendedMetadata = {
13 ruleName: 'no-feature-envy',
14 type: 'maintainability', // one of: 'functionality' | 'maintainability' | 'style' | 'typescript'
15 description: 'A method accesses the data of another object more than its own data.',
16 options: null,
17 optionsDescription: '',
18 optionExamples: [], //Remove this property if the rule has no options
19 recommendation: '[true, 1, ["_"]],',
20 typescriptOnly: false,
21 issueClass: 'Non-SDL', // one of: 'SDL' | 'Non-SDL' | 'Ignored'
22 issueType: 'Warning', // one of: 'Error' | 'Warning'
23 severity: 'Moderate', // one of: 'Critical' | 'Important' | 'Moderate' | 'Low'
24 level: 'Opportunity for Excellence', // one of 'Mandatory' | 'Opportunity for Excellence'
25 group: 'Clarity', // one of 'Ignored' | 'Security' | 'Correctness' | 'Clarity' | 'Whitespace' | 'Configurable' | 'Deprecated'
26 commonWeaknessEnumeration: '', // if possible, please map your rule to a CWE (see cwe_descriptions.json and https://cwe.mitre.org)
27 };
28
29 public static FAILURE_STRING(feature: MethodFeature): string {
30 const { methodName, className, otherClassName } = feature;
31 const failureMessage = `Method "${methodName}" uses "${otherClassName}" more than its own class "${className}".`;
32 const recommendation = `Extract or Move Method from "${methodName}" into "${otherClassName}".`;
33 return `${failureMessage} ${recommendation}`;
34 }
35
36 public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
37 return this.applyWithWalker(new NoFeatureEnvyRuleWalker(sourceFile, this.getOptions()));
38 }
39}
40
41class NoFeatureEnvyRuleWalker extends ErrorTolerantWalker {
42 private threshold: number = 0;
43 private exclude: string[] = [];
44
45 constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
46 super(sourceFile, options);
47 this.parseOptions();
48 }
49
50 protected visitClassDeclaration(node: ts.ClassDeclaration): void {
51 this.checkAndReport(node);
52 super.visitClassDeclaration(node);
53 }
54
55 private checkAndReport(node: ts.ClassDeclaration): void {
56 this.getFeatureMethodsForClass(node).forEach(feature => {
57 const failureMessage = Rule.FAILURE_STRING(feature);
58 this.addFailureAtNode(feature.methodNode, failureMessage);
59 });
60 }
61
62 private getFeatureMethodsForClass(classNode: ts.ClassDeclaration): MethodFeature[] {
63 const methods = this.methodsForClass(classNode);
64 return <any[]>methods
65 .map(method => {
66 const walker = new ClassMethodWalker(classNode, method);
67 return walker.features();
68 })
69 .map(features => this.getTopFeature(features))
70 .filter(feature => feature !== undefined);
71 }
72
73 private getTopFeature(features: MethodFeature[]): MethodFeature | void {
74 const filteredFeatures = this.filterFeatures(features);
75 return filteredFeatures.reduce((best, current) => {
76 if (!best) {
77 return current;
78 }
79 if (current.featureEnvy() > best.featureEnvy()) {
80 return current;
81 }
82 return best;
83 }, undefined);
84 }
85
86 private filterFeatures(features: MethodFeature[]): MethodFeature[] {
87 return features.filter(feature => {
88 const isExcluded = this.exclude.indexOf(feature.otherClassName) !== -1;
89 if (isExcluded) {
90 return false;
91 }
92 return feature.featureEnvy() > this.threshold;
93 });
94 }
95
96 protected methodsForClass(classNode: ts.ClassDeclaration): ts.MethodDeclaration[] {
97 return <ts.MethodDeclaration[]>classNode.members.filter(
98 (classElement: ts.ClassElement): boolean => {
99 switch (classElement.kind) {
100 case ts.SyntaxKind.MethodDeclaration:
101 case ts.SyntaxKind.GetAccessor:
102 case ts.SyntaxKind.SetAccessor:
103 return !AstUtils.isStatic(classElement);
104 default:
105 return false;
106 }
107 }
108 );
109 }
110
111 private parseOptions(): void {
112 this.getOptions().forEach((opt: any) => {
113 if (typeof opt === 'boolean') {
114 return;
115 }
116 if (typeof opt === 'number') {
117 this.threshold = opt;
118 return;
119 }
120 if (Array.isArray(opt)) {
121 this.exclude = opt;
122 return;
123 }
124 });
125 }
126}
127
128class ClassMethodWalker extends Lint.SyntaxWalker {
129 private featureEnvyMap: EnvyMap = {};
130
131 constructor(private classNode: ts.ClassDeclaration, private methodNode: ts.MethodDeclaration) {
132 super();
133 this.walk(this.methodNode);
134 }
135
136 public features(): MethodFeature[] {
137 const thisClassAccesses = this.getCountForClass('this');
138 return this.classesUsed.map(className => {
139 const otherClassAccesses = this.getCountForClass(className);
140 return new MethodFeature({
141 classNode: this.classNode,
142 methodNode: this.methodNode,
143 otherClassName: className,
144 thisClassAccesses,
145 otherClassAccesses,
146 });
147 });
148 }
149
150 private getCountForClass(className: string): number {
151 return this.featureEnvyMap[className] || 0;
152 }
153
154 private get classesUsed(): string[] {
155 return Object.keys(this.featureEnvyMap).filter(className => className !== 'this');
156 }
157
158 protected visitPropertyAccessExpression(node: ts.PropertyAccessExpression) {
159 if (this.isTopPropertyAccess(node)) {
160 const className = this.classNameForPropertyAccess(node);
161 this.incrementCountForClass(className);
162 }
163 super.visitPropertyAccessExpression(node);
164 }
165
166 private incrementCountForClass(className: string): void {
167 if (this.featureEnvyMap[className] !== undefined) {
168 this.featureEnvyMap[className] += 1;
169 } else {
170 this.featureEnvyMap[className] = 1;
171 }
172 }
173
174 private isTopPropertyAccess(node: ts.PropertyAccessExpression): boolean {
175 switch (node.expression.kind) {
176 case ts.SyntaxKind.Identifier:
177 case ts.SyntaxKind.ThisKeyword:
178 case ts.SyntaxKind.SuperKeyword:
179 return true;
180 }
181 return false;
182 }
183
184 private classNameForPropertyAccess(node: ts.PropertyAccessExpression): string {
185 const { expression } = node;
186 if (ts.isThisTypeNode(node)) {
187 return 'this';
188 }
189 if (expression.kind === ts.SyntaxKind.SuperKeyword) {
190 return 'this';
191 }
192 if (this.classNode.name.getText() === expression.getText()) {
193 return 'this';
194 }
195 return expression.getText();
196 }
197}
198
199export class MethodFeature {
200 constructor(
201 private data: {
202 classNode: ts.ClassDeclaration;
203 methodNode: ts.MethodDeclaration;
204 otherClassName: string;
205 thisClassAccesses: number;
206 otherClassAccesses: number;
207 }
208 ) {}
209
210 public get className(): string {
211 return this.classNode.name.text;
212 }
213 public get classNode(): ts.ClassDeclaration {
214 return this.data.classNode;
215 }
216
217 public get methodName(): string {
218 return this.methodNode.name.getText();
219 }
220 public get methodNode(): ts.MethodDeclaration {
221 return this.data.methodNode;
222 }
223
224 public featureEnvy(): number {
225 const { thisClassAccesses, otherClassAccesses } = this.data;
226 return otherClassAccesses - thisClassAccesses;
227 }
228
229 public get otherClassName(): string {
230 return this.data.otherClassName;
231 }
232}
233
234interface EnvyMap {
235 [className: string]: number;
236}