UNPKG

7.74 kBPlain TextView Raw
1import * as ts from 'typescript';
2import * as Lint from 'tslint';
3
4import { ErrorTolerantWalker } from './utils/ErrorTolerantWalker';
5import { AstUtils } from './utils/AstUtils';
6import { Utils } from './utils/Utils';
7import { ExtendedMetadata } from './utils/ExtendedMetadata';
8
9const FAILURE_STRING: string = 'The cohesion of this class is too low. Consider splitting this class into multiple cohesive classes: ';
10
11/**
12 * Implementation of the min-class-cohesion rule.
13 */
14export class Rule extends Lint.Rules.AbstractRule {
15 public static metadata: ExtendedMetadata = {
16 ruleName: 'min-class-cohesion',
17 type: 'maintainability',
18 description: 'High cohesion means the methods and variables of the class are co-dependent and hang together as a logical whole.',
19 options: null,
20 optionsDescription: '',
21 typescriptOnly: true,
22 issueClass: 'Non-SDL',
23 issueType: 'Warning',
24 severity: 'Important',
25 level: 'Opportunity for Excellence',
26 group: 'Correctness',
27 recommendation: '[true, 0.5],',
28 commonWeaknessEnumeration: '',
29 };
30
31 public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
32 return this.applyWithWalker(new MinClassCohesionRuleWalker(sourceFile, this.getOptions()));
33 }
34}
35
36class MinClassCohesionRuleWalker extends ErrorTolerantWalker {
37 private minClassCohesion: number = 0.5;
38
39 constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
40 super(sourceFile, options);
41 this.parseOptions();
42 }
43
44 protected visitClassDeclaration(node: ts.ClassDeclaration): void {
45 if (!this.isClassCohesive(node)) {
46 const className: string = node.name == null ? '<unknown>' : node.name.text;
47 this.addFailureAt(node.getStart(), node.getWidth(), FAILURE_STRING + className);
48 }
49 super.visitClassDeclaration(node);
50 }
51
52 private isClassCohesive(node: ts.ClassDeclaration): boolean {
53 const classNode = new ClassDeclarationHelper(node);
54 if (classNode.extendsSomething) {
55 return true;
56 }
57 const { cohesionScore } = classNode;
58 // console.log('Cohesion:', cohesionScore); // tslint:disable-line no-console
59 return cohesionScore >= this.minClassCohesion;
60 }
61
62 private parseOptions(): void {
63 this.getOptions().forEach((opt: any) => {
64 if (typeof opt === 'boolean') {
65 return;
66 }
67 if (typeof opt === 'number') {
68 this.minClassCohesion = opt;
69 return;
70 }
71 throw new Error(`Rule min-class-cohesion only supports option of type number, not ${typeof opt}.`);
72 });
73 }
74}
75
76class ClassDeclarationHelper {
77 constructor(private node: ts.ClassDeclaration) {}
78
79 public get cohesionScore(): number {
80 const { fieldNames, methods } = this;
81 // console.log('================='); // tslint:disable-line no-console
82 // console.log('Class:', this.name); // tslint:disable-line no-console
83 // console.log('Field names:', fieldNames); // tslint:disable-line no-console
84 // console.log('Methods:', methods); // tslint:disable-line no-console
85 if (methods.length === 0) {
86 return 1.0;
87 }
88 const numFields = fieldNames.length;
89 if (numFields === 0) {
90 return 0.0;
91 }
92 const methodScores = methods.map(method => {
93 const used = this.numberOfFieldsUsedByMethod(fieldNames, method);
94 return used / numFields;
95 });
96 const sumScores = methodScores.reduce((total, current) => total + current, 0);
97 return sumScores / methods.length;
98 // console.log('Average score:', avgScore); // tslint:disable-line no-console
99 }
100
101 private get fieldNames(): string[] {
102 const parameterNames: string[] = this.constructorParameterNames;
103 const instanceFields: string[] = this.instanceFieldNames;
104 return [...parameterNames, ...instanceFields];
105 }
106
107 private get methods(): ts.MethodDeclaration[] {
108 return <ts.MethodDeclaration[]>this.node.members.filter(
109 (classElement: ts.ClassElement): boolean => {
110 switch (classElement.kind) {
111 case ts.SyntaxKind.MethodDeclaration:
112 case ts.SyntaxKind.GetAccessor:
113 case ts.SyntaxKind.SetAccessor:
114 return !AstUtils.isStatic(classElement);
115 default:
116 return false;
117 }
118 }
119 );
120 }
121
122 private numberOfFieldsUsedByMethod(fieldNames: string[], method: ts.MethodDeclaration): number {
123 const fields = ClassDeclarationHelper.fieldsUsedByMethod(method);
124 return fieldNames.reduce((count, fieldName) => {
125 if (fields[fieldName]) {
126 return count + 1;
127 }
128 return count;
129 }, 0);
130 }
131
132 private get constructorParameterNames(): string[] {
133 return this.constructorParameters.map(param => param.name.getText());
134 }
135
136 private get constructorParameters(): ts.ParameterDeclaration[] {
137 const ctor: ts.ConstructorDeclaration = this.constructorDeclaration;
138 if (ctor) {
139 return ctor.parameters.filter(
140 (param: ts.ParameterDeclaration): boolean => {
141 return (
142 AstUtils.hasModifier(param.modifiers, ts.SyntaxKind.PublicKeyword) ||
143 AstUtils.hasModifier(param.modifiers, ts.SyntaxKind.PrivateKeyword) ||
144 AstUtils.hasModifier(param.modifiers, ts.SyntaxKind.ProtectedKeyword) ||
145 AstUtils.hasModifier(param.modifiers, ts.SyntaxKind.ReadonlyKeyword)
146 );
147 }
148 );
149 }
150 return [];
151 }
152
153 private get constructorDeclaration(): ts.ConstructorDeclaration | undefined {
154 return <ts.ConstructorDeclaration>(
155 this.node.members.find((element: ts.ClassElement): boolean => element.kind === ts.SyntaxKind.Constructor)
156 );
157 }
158
159 private get instanceFieldNames(): string[] {
160 return this.instanceFields.map(param => param.name.getText());
161 }
162
163 private get instanceFields(): ts.PropertyDeclaration[] {
164 return <ts.PropertyDeclaration[]>(
165 this.node.members.filter((classElement: ts.ClassElement): boolean => classElement.kind === ts.SyntaxKind.PropertyDeclaration)
166 );
167 }
168
169 private static fieldsUsedByMethod(method: ts.MethodDeclaration): FieldsUsageMap {
170 const walker = new ClassMethodWalker();
171 walker.walk(method);
172 return walker.fieldsUsed;
173 }
174
175 public get extendsSomething(): boolean {
176 return Utils.exists(
177 this.node.heritageClauses,
178 (clause: ts.HeritageClause): boolean => {
179 return clause.token === ts.SyntaxKind.ExtendsKeyword;
180 }
181 );
182 }
183
184 public get name() {
185 return this.node.name == null ? '<unknown>' : this.node.name.text;
186 }
187}
188
189class ClassMethodWalker extends Lint.SyntaxWalker {
190 public fieldsUsed: FieldsUsageMap = {};
191
192 protected visitPropertyAccessExpression(node: ts.PropertyAccessExpression): void {
193 const isOnThis = node.expression.kind === ts.SyntaxKind.ThisKeyword;
194 if (isOnThis) {
195 const field = node.name.text;
196 // console.log('visitPropertyAccessExpression:', field, node.expression); // tslint:disable-line no-console
197 this.fieldsUsed[field] = true;
198 }
199 super.visitPropertyAccessExpression(node);
200 }
201}
202
203interface FieldsUsageMap {
204 [field: string]: boolean;
205}