/
contextualDecoratorRule.ts
112 lines (90 loc) · 3.61 KB
/
contextualDecoratorRule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { sprintf } from 'sprintf-js';
import { IRuleMetadata, RuleFailure, WalkContext } from 'tslint';
import { AbstractRule } from 'tslint/lib/rules';
import { dedent } from 'tslint/lib/utils';
import {
AccessorDeclaration,
createNodeArray,
Decorator,
forEachChild,
isAccessor,
isMethodDeclaration,
isParameterPropertyDeclaration,
isPropertyDeclaration,
MethodDeclaration,
Node,
ParameterPropertyDeclaration,
PropertyDeclaration,
SourceFile
} from 'typescript';
import { isNotNullOrUndefined } from './util/isNotNullOrUndefined';
import {
ANGULAR_CLASS_DECORATOR_MAPPER,
AngularClassDecoratorKeys,
AngularClassDecorators,
AngularInnerClassDecorators,
getDecoratorName,
getNextToLastParentNode,
isAngularClassDecorator,
isAngularInnerClassDecorator
} from './util/utils';
interface FailureParameters {
readonly classDecoratorName: AngularClassDecoratorKeys;
}
type DeclarationLike = AccessorDeclaration | MethodDeclaration | ParameterPropertyDeclaration | PropertyDeclaration;
export const getFailureMessage = (failureParameters: FailureParameters): string => {
return sprintf(Rule.FAILURE_STRING, failureParameters.classDecoratorName);
};
export class Rule extends AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Ensures that classes use contextual decorators in its body.',
options: null,
optionsDescription: 'Not configurable.',
rationale: dedent`
Some decorators should only be used in certain class types. For example,
the decorator @${AngularInnerClassDecorators.Input}() should
not be used in a class decorated with @${AngularClassDecorators.Injectable}().
`,
ruleName: 'contextual-decorator',
type: 'functionality',
typescriptOnly: true
};
static readonly FAILURE_STRING = 'Decorator out of context for "@%s()"';
apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
const callbackHandler = (walkContext: WalkContext, node: Node): void => {
if (isDeclarationLike(node)) validateDeclaration(walkContext, node);
};
const getClassDecoratorName = (klass: Node): AngularClassDecoratorKeys | undefined => {
return createNodeArray(klass.decorators)
.map(getDecoratorName)
.filter(isNotNullOrUndefined)
.find(isAngularClassDecorator);
};
const isDeclarationLike = (node: Node): node is DeclarationLike => {
return isAccessor(node) || isMethodDeclaration(node) || isParameterPropertyDeclaration(node, node.parent) || isPropertyDeclaration(node);
};
const validateDeclaration = (walkContext: WalkContext, node: DeclarationLike): void => {
const klass = getNextToLastParentNode(node);
const classDecoratorName = getClassDecoratorName(klass);
if (!classDecoratorName) return;
createNodeArray(node.decorators).forEach(decorator => validateDecorator(walkContext, decorator, classDecoratorName));
};
const validateDecorator = (walkContext: WalkContext, node: Decorator, classDecoratorName: AngularClassDecoratorKeys): void => {
const decoratorName = getDecoratorName(node);
if (!decoratorName || !isAngularInnerClassDecorator(decoratorName)) return;
const allowedDecorators = ANGULAR_CLASS_DECORATOR_MAPPER.get(classDecoratorName);
if (!allowedDecorators || allowedDecorators.has(decoratorName)) return;
const failure = getFailureMessage({ classDecoratorName });
walkContext.addFailureAtNode(node, failure);
};
const walk = (walkContext: WalkContext): void => {
const { sourceFile } = walkContext;
const callback = (node: Node): void => {
callbackHandler(walkContext, node);
forEachChild(node, callback);
};
forEachChild(sourceFile, callback);
};