UNPKG

16.8 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';
8import { DirectedAcyclicGraph } from './utils/DirectedAcyclicGraph';
9import * as Memoize from 'memoize-decorator';
10
11export const FAILURE_CLASS_STRING: string = 'The class does not read like a Newspaper. Reorder the methods of the class: ';
12export const FAILURE_FILE_STRING: string = 'The functions in the file do not read like a Newspaper. Reorder the functions in the file: ';
13export const FAILURE_BLOCK_STRING: string = 'The functions in the block do not read like a Newspaper. Reorder the functions in the block: ';
14const END_OF_DEPENDENCIES_MARKER: string = '--end-of-dependencies-marker--';
15
16/**
17 * Implementation of the newspaper-order rule.
18 */
19export class Rule extends Lint.Rules.AbstractRule {
20 public static metadata: ExtendedMetadata = {
21 ruleName: 'newspaper-order',
22 type: 'maintainability',
23 description:
24 'We would like a source file to be like a newspaper article. ' +
25 'Detail should increase as we move downward, ' +
26 'until at the end we find the lowest level functions and details in the source file.',
27 options: null,
28 optionsDescription: '',
29 typescriptOnly: true,
30 issueClass: 'Non-SDL',
31 issueType: 'Warning',
32 severity: 'Important',
33 level: 'Opportunity for Excellence',
34 group: 'Correctness',
35 recommendation: 'true,',
36 commonWeaknessEnumeration: '',
37 };
38
39 public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
40 return this.applyWithWalker(new NewspaperOrderRuleWalker(sourceFile, this.getOptions()));
41 }
42}
43
44class NewspaperOrderRuleWalker extends ErrorTolerantWalker {
45 protected visitBlock(node: ts.Block): void {
46 const blockNode = new BlockHelper(node);
47 this.checkAndReportFailure(blockNode, FAILURE_BLOCK_STRING);
48 super.visitBlock(node);
49 }
50
51 protected visitClassDeclaration(node: ts.ClassDeclaration): void {
52 const classNode = new ClassDeclarationHelper(node);
53 this.checkAndReportFailure(classNode, FAILURE_CLASS_STRING);
54 super.visitClassDeclaration(node);
55 }
56
57 protected visitSourceFile(node: ts.SourceFile): void {
58 const sourceNode = new SourceFileHelper(node);
59 this.checkAndReportFailure(sourceNode, FAILURE_FILE_STRING);
60 super.visitSourceFile(node);
61 }
62
63 private checkAndReportFailure(nodeHelper: NewspaperHelper, failureString: string) {
64 if (!this.readsLikeNewspaper(nodeHelper)) {
65 const failureMessage = this.makeClassFailureMessage(nodeHelper, failureString);
66 this.addFailureAt(nodeHelper.start, nodeHelper.width, failureMessage);
67 }
68 }
69
70 private makeClassFailureMessage(nodeHelper: NewspaperHelper, failureString: string): string {
71 const { nodeName, completeOrderedMethodNames, methodNames } = nodeHelper;
72 const correctSymbol = '✓';
73 const incorrectSymbol = 'x';
74 const help: string =
75 '\n\nMethods order:\n' +
76 completeOrderedMethodNames
77 .map((method, index) => {
78 const isCorrect = methodNames[index] === method;
79 const status = isCorrect ? correctSymbol : incorrectSymbol;
80 return `${index + 1}. ${status} ${method}`;
81 })
82 .join('\n');
83 return failureString + nodeName + help;
84 }
85
86 private readsLikeNewspaper(nodeHelper: NewspaperHelper): boolean {
87 return nodeHelper.readsLikeNewspaper;
88 }
89}
90
91abstract class NewspaperHelper {
92 constructor(protected node: ts.Node) {}
93
94 @Memoize
95 public get readsLikeNewspaper(): boolean {
96 // console.log('====================='); // tslint:disable-line no-console
97 // console.log('Node: ', nodeName); // tslint:disable-line no-console
98 const { methodNames, completeOrderedMethodNames, ignoredMethods } = this;
99 const ignoringAllMethods: boolean = ignoredMethods.length === methodNames.length;
100 const hasNoDeps: boolean = completeOrderedMethodNames.length === 0;
101 // console.log('ignoredMethods:', ignoredMethods); // tslint:disable-line no-console
102 // console.log('methodNames:', methodNames); // tslint:disable-line no-console
103 // console.log('orderedMethodNames:', completeOrderedMethodNames); // tslint:disable-line no-console
104 if (ignoringAllMethods || hasNoDeps) {
105 return true;
106 }
107 return Utils.arraysShallowEqual(methodNames, completeOrderedMethodNames);
108 }
109
110 public get width(): number {
111 return this.end - this.start;
112 }
113
114 @Memoize
115 private get end(): number {
116 const len = this.incorrectMethodNames.length;
117 const lastIncorrectFunctionName: string | undefined = len > 0 ? this.incorrectMethodNames[len - 1] : undefined;
118 if (lastIncorrectFunctionName) {
119 const lastIncorrectFunction = this.methodForName(lastIncorrectFunctionName);
120 return lastIncorrectFunction.getEnd();
121 }
122 return this.node.getEnd();
123 }
124
125 @Memoize
126 public get start(): number {
127 const firstIncorrectFunctionName: string | undefined = this.incorrectMethodNames[0];
128 if (firstIncorrectFunctionName) {
129 const firstIncorrectFunction = this.methodForName(firstIncorrectFunctionName);
130 return firstIncorrectFunction.getStart();
131 }
132 return this.node.getStart();
133 }
134
135 @Memoize
136 protected get incorrectMethodNames(): string[] {
137 const { completeOrderedMethodNames, methodNames } = this;
138 return methodNames.filter((methodName, index) => {
139 return methodName !== completeOrderedMethodNames[index];
140 });
141 }
142
143 @Memoize
144 public get completeOrderedMethodNames(): string[] {
145 const { orderedMethodNames, ignoredMethods } = this;
146 return orderedMethodNames.concat(ignoredMethods);
147 }
148
149 @Memoize
150 protected get ignoredMethods(): string[] {
151 const { methodNames, orderedMethodNames } = this;
152 return methodNames.filter(methodName => {
153 return orderedMethodNames.indexOf(methodName) === -1;
154 });
155 }
156
157 @Memoize
158 private get orderedMethodNames(): string[] {
159 const { methodGraph, methodNames } = this;
160 try {
161 const top = new TopologicalSortUtil(methodGraph);
162 const ordered = top.closestList(methodNames);
163 if (ordered[ordered.length - 1] === END_OF_DEPENDENCIES_MARKER) {
164 ordered.pop();
165 }
166 return ordered;
167 } catch (error) {
168 return [];
169 }
170 }
171
172 @Memoize
173 private get methodGraph(): DependencyGraph {
174 const { methodDependencies } = this;
175 // console.log('methodDependencies:', methodDependencies); // tslint:disable-line no-console
176 return Object.keys(methodDependencies)
177 .sort()
178 .reduce(
179 (graph: DependencyGraph, methodName: string) => {
180 const deps = Object.keys(methodDependencies[methodName]).sort();
181 deps.forEach(depName => {
182 const shouldIgnore: boolean = !methodDependencies.hasOwnProperty(depName) || methodName === depName;
183 // console.log('shouldIgnore:', shouldIgnore, methodName, depName); // tslint:disable-line no-console
184 if (shouldIgnore) {
185 return;
186 }
187 const edge = [methodName, depName];
188 graph.push(edge);
189 });
190 graph.push([methodName, END_OF_DEPENDENCIES_MARKER]);
191 // console.log('graph:', graph); // tslint:disable-line no-console
192 return graph;
193 },
194 <DependencyGraph>[]
195 );
196 }
197
198 @Memoize
199 private get methodDependencies(): MethodDependenciesMap {
200 return this.methods.reduce(
201 (result, method) => {
202 result[method.name.getText()] = this.dependenciesForMethod(method);
203 return result;
204 },
205 <MethodDependenciesMap>{}
206 );
207 }
208
209 protected abstract dependenciesForMethod(method: ts.FunctionLikeDeclaration): MethodDependencies;
210
211 @Memoize
212 public get methodNames(): string[] {
213 return this.methods.map(method => method.name.getText());
214 }
215
216 protected methodForName(methodName: string): ts.FunctionLikeDeclaration {
217 return this.methodsIndex[methodName];
218 }
219
220 @Memoize
221 protected get methodsIndex(): { [methodName: string]: ts.FunctionLikeDeclaration } {
222 return this.methods.reduce((index, method) => {
223 const name = method.name.getText();
224 index[name] = method;
225 return index;
226 }, {});
227 }
228
229 protected abstract get methods(): ts.FunctionLikeDeclaration[];
230
231 abstract get nodeName(): string;
232}
233
234class ClassDeclarationHelper extends NewspaperHelper {
235 constructor(protected node: ts.ClassDeclaration) {
236 super(node);
237 }
238
239 protected dependenciesForMethod(method: ts.MethodDeclaration): MethodDependencies {
240 const walker = new ClassMethodWalker();
241 walker.walk(method);
242 return walker.dependencies;
243 }
244
245 @Memoize
246 protected get methods(): ts.MethodDeclaration[] {
247 return <ts.MethodDeclaration[]>this.node.members.filter(
248 (classElement: ts.ClassElement): boolean => {
249 switch (classElement.kind) {
250 case ts.SyntaxKind.MethodDeclaration:
251 case ts.SyntaxKind.GetAccessor:
252 case ts.SyntaxKind.SetAccessor:
253 return !AstUtils.isStatic(classElement);
254 default:
255 return false;
256 }
257 }
258 );
259 }
260
261 @Memoize
262 public get nodeName() {
263 return this.node.name == null ? '<unknown>' : this.node.name.text;
264 }
265}
266
267abstract class BlockLikeHelper extends NewspaperHelper {
268 constructor(protected node: ts.BlockLike) {
269 super(node);
270 }
271
272 @Memoize
273 protected get methods(): ts.FunctionDeclaration[] {
274 const functionDeclarations = <ts.FunctionDeclaration[]>(
275 this.node.statements.filter((node: ts.Statement): boolean => ts.isFunctionDeclaration(node))
276 );
277 const variableStatements = <ts.VariableStatement[]>(
278 this.node.statements.filter((node: ts.Statement): boolean => ts.isVariableStatement(node))
279 );
280 const variableFunctionDeclarations: ts.FunctionDeclaration[] = variableStatements
281 .map(node => node.declarationList.declarations)
282 .map(declarations => declarations.map(this.createFuncDeclarFromVarDeclar).filter(node => node))
283 .reduce((result, item) => [...result, ...item], []);
284 return [...functionDeclarations, ...variableFunctionDeclarations];
285 }
286
287 private createFuncDeclarFromVarDeclar(declaration: ts.VariableDeclaration): ts.FunctionDeclaration | null {
288 const { name, initializer } = declaration;
289 if (ts.isIdentifier(name) && ts.isFunctionExpression(initializer)) {
290 const node = ts.createFunctionDeclaration([], [], undefined, name, [], [], undefined, initializer.body);
291 node.pos = declaration.pos;
292 node.end = declaration.end;
293 return node;
294 }
295 return null;
296 }
297
298 protected dependenciesForMethod(method: ts.FunctionDeclaration): MethodDependencies {
299 const walker = new FunctionWalker();
300 walker.walk(method);
301 return walker.dependencies;
302 }
303}
304
305class SourceFileHelper extends BlockLikeHelper {
306 constructor(protected node: ts.SourceFile) {
307 super(node);
308 }
309
310 @Memoize
311 public get nodeName() {
312 return this.node.fileName == null ? '<unknown>' : this.node.fileName;
313 }
314}
315
316class BlockHelper extends BlockLikeHelper {
317 constructor(protected node: ts.Block) {
318 super(node);
319 }
320
321 @Memoize
322 public get nodeName() {
323 const { node } = this;
324 if (node.parent) {
325 if (node.parent.kind === ts.SyntaxKind.FunctionDeclaration) {
326 return (<ts.FunctionDeclaration>node.parent).name.getText() || '<anonymous>';
327 }
328 }
329 return '<anonymous>';
330 }
331}
332
333class ClassMethodWalker extends Lint.SyntaxWalker {
334 public dependencies: MethodDependencies = {};
335
336 protected visitPropertyAccessExpression(node: ts.PropertyAccessExpression): void {
337 const isOnThis = node.expression.kind === ts.SyntaxKind.ThisKeyword;
338 if (isOnThis) {
339 const field = node.name.text;
340 this.dependencies[field] = true;
341 }
342 super.visitPropertyAccessExpression(node);
343 }
344
345 protected visitBindingElement(node: ts.BindingElement): void {
346 const isOnThis = node.parent.parent.initializer.kind === ts.SyntaxKind.ThisKeyword;
347 if (isOnThis) {
348 const field = node.name.getText();
349 this.dependencies[field] = true;
350 }
351 super.visitBindingElement(node);
352 }
353}
354
355class FunctionWalker extends Lint.SyntaxWalker {
356 public dependencies: MethodDependencies = {};
357
358 protected visitCallExpression(node: ts.CallExpression): void {
359 if (node.expression.kind === ts.SyntaxKind.Identifier) {
360 const field = node.expression.getText();
361 this.dependencies[field] = true;
362 }
363 if (Array.isArray(node.arguments)) {
364 node.arguments.forEach(arg => {
365 if (arg.kind === ts.SyntaxKind.Identifier) {
366 this.dependencies[arg.getText()] = true;
367 }
368 });
369 }
370 super.visitCallExpression(node);
371 }
372}
373
374interface MethodDependenciesMap {
375 [methodName: string]: MethodDependencies;
376}
377
378interface MethodDependencies {
379 [dependencyMethodName: string]: true;
380}
381
382type DependencyGraph = string[][];
383
384class TopologicalSortUtil {
385 constructor(private graph: DependencyGraph) {}
386
387 public closestList(currentList: string[]): string[] {
388 if (currentList.length === 0) {
389 return [];
390 }
391 const { allLists } = this;
392 if (allLists.length === 0) {
393 return [];
394 }
395 let bestList: string[] = [];
396 let bestDistance: number = Infinity;
397 allLists.forEach(list => {
398 const dist = this.distanceBetweenLists(currentList, list);
399 if (dist < bestDistance) {
400 bestDistance = dist;
401 bestList = list;
402 }
403 });
404 // console.log('closestList', bestList, bestDistance, currentList, allLists, this.graph); // tslint:disable-line no-console
405 return bestList;
406 }
407
408 private distanceBetweenLists(srcList: string[], destList: string[]): number {
409 const positionMap = destList.reduce((result, key, index) => {
410 result[key] = index;
411 return result;
412 }, {});
413 return srcList.reduce((total, key, index) => {
414 const destIndex: number | undefined = positionMap[key];
415 if (destIndex) {
416 return total + Math.abs(destIndex - index);
417 }
418 return total;
419 }, 0);
420 }
421
422 @Memoize
423 private get allLists(): string[][] {
424 const { dag, list } = this;
425 // console.log('list', list); // tslint:disable-line no-console
426 const indexMap: { [index: number]: string } = list.reduce((result, key, index) => {
427 result[index] = key;
428 return result;
429 }, {});
430 return dag.alltopologicalSort().map(currList => {
431 return currList.map(index => indexMap[index]);
432 });
433 }
434
435 @Memoize
436 private get dag(): DirectedAcyclicGraph {
437 const { graph, list } = this;
438 const positionMap = list.reduce((result, key, index) => {
439 result[key] = index;
440 return result;
441 }, {});
442 const dag: DirectedAcyclicGraph = new DirectedAcyclicGraph(list.length);
443 graph.forEach(([from, to]) => {
444 const ai = positionMap[from];
445 const bi = positionMap[to];
446 dag.addEdge(ai, bi);
447 });
448 return dag;
449 }
450
451 @Memoize
452 private get list() {
453 const { graph } = this;
454 const index: { [key: string]: true } = {};
455 graph.forEach(([from, to]) => {
456 index[from] = true;
457 index[to] = true;
458 });
459 return Object.keys(index);
460 }
461}