16
16
17
17
package com.netflix.nebula.lint.rule
18
18
19
+ import org.codehaus.groovy.ast.expr.*
20
+ import org.gradle.api.Action
19
21
import org.gradle.api.Project
22
+ import org.gradle.api.Task
23
+ import org.gradle.api.internal.DefaultDomainObjectCollection
24
+ import org.gradle.api.plugins.ExtensionAware
25
+ import org.gradle.configuration.ImportsReader
26
+
27
+ import javax.annotation.Nullable
20
28
21
29
/**
22
30
* Decorate lint rule visitors with this interface in order to use the
23
31
* evaluated Gradle project model in the rule
24
32
*/
25
- interface GradleModelAware {
26
- void setProject (Project project )
33
+ trait GradleModelAware {
34
+ Project project
35
+ Map<String , List<String > > projectDefaultImports = null
36
+
37
+ TypeInformation receiver(MethodCallExpression call) {
38
+ List<Expression > fullCallStack = typedDslStack(callStack + call)
39
+ List<TypeInformation > typedStack = []
40
+ for (Expression currentMethod in fullCallStack) {
41
+ if (typedStack. empty) {
42
+ typedStack. add(new TypeInformation (project))
43
+ }
44
+ while (! typedStack. empty) {
45
+ def current = typedStack. last()
46
+ def candidate = findDirectCandidate(current, currentMethod)
47
+ if (candidate != null ) {
48
+ typedStack. add(candidate)
49
+ break
50
+ }
51
+ typedStack. removeLast()
52
+ }
53
+ }
54
+ if (typedStack. size() >= 2 ) { // there should be the method return type and the receiver at least
55
+ return typedStack[-2 ]
56
+ } else {
57
+ return null
58
+ }
59
+ }
60
+
61
+ private findDirectCandidate(TypeInformation current, Expression currentExpression) {
62
+ String methodName
63
+ switch (currentExpression) {
64
+ case MethodCallExpression :
65
+ methodName = currentExpression. methodAsString
66
+ break
67
+ case PropertyExpression :
68
+ methodName = currentExpression. propertyAsString
69
+ break
70
+ case VariableExpression :
71
+ methodName = currentExpression. text
72
+ break
73
+ case ConstantExpression :
74
+ methodName = currentExpression. text
75
+ break
76
+ default :
77
+ return null
78
+ }
79
+ def getter = current. clazz. getMethods(). find { it. name == " get${ methodName.capitalize()} " }
80
+ if (getter != null ) {
81
+ if (current. object != null ) {
82
+ try {
83
+ return new TypeInformation (getter. invoke(current. object))
84
+ } catch (ignored) {
85
+ // ignore and fallback to the return type
86
+ }
87
+ }
88
+ return new TypeInformation (null , getter. returnType)
89
+ }
90
+
91
+ // there is no public API for DomainObjectCollection.type
92
+ if (current. object != null && DefaultDomainObjectCollection . class. isAssignableFrom(current. clazz)) {
93
+ def collectionItemType = ((DefaultDomainObjectCollection ) current. object). type
94
+
95
+ if (methodName == " withType" && currentExpression instanceof MethodCallExpression && currentExpression. arguments. size() >= 1 ) {
96
+ def className = currentExpression. arguments[0 ]
97
+ def candidate = findSuitableClass(className. text, collectionItemType)
98
+ if (candidate != null ) {
99
+ collectionItemType = candidate
100
+ }
101
+ }
102
+
103
+ if ((methodName == " create" || methodName == " register" ) && currentExpression instanceof MethodCallExpression && currentExpression. arguments. size() >= 2 && currentExpression. arguments[1 ] ! instanceof ClosureExpression ) {
104
+ def className = currentExpression. arguments[1 ]
105
+ def candidate = findSuitableClass(className. text, collectionItemType)
106
+ if (candidate != null ) {
107
+ collectionItemType = candidate
108
+ }
109
+ }
110
+
111
+ def transformationOrFactoryMethod = current. clazz. getMethods(). find { it. name == methodName && it. parameterTypes[-1 ] == Action . class }
112
+ if (transformationOrFactoryMethod != null ) {
113
+ if (collectionItemType. isAssignableFrom(transformationOrFactoryMethod. returnType)) {
114
+ return new TypeInformation (null , transformationOrFactoryMethod. returnType)
115
+ } else {
116
+ // assume that all actions are done on the collection type
117
+ return new TypeInformation (null , collectionItemType)
118
+ }
119
+ }
120
+ }
121
+
122
+ // note that we can't use tasks.findByName because it may lead to unwanted side effects because of potential task creation
123
+ if (Project . class. isAssignableFrom(current. clazz) && methodName == " task" && currentExpression instanceof MethodCallExpression ) {
124
+ def taskType = extractTaskType(currentExpression)
125
+ if (taskType != null ) {
126
+ return new TypeInformation (null , taskType)
127
+ }
128
+ return new TypeInformation (null , Task . class)
129
+ }
130
+
131
+ def factoryMethod = current. clazz. getMethods(). find { it. name == methodName && it. parameterTypes[-1 ] == Action . class }
132
+ if (factoryMethod != null ) {
133
+ // assume that this is a factory method that returns the created type
134
+ return new TypeInformation (null , factoryMethod. returnType)
135
+ }
136
+
137
+ if (current. object != null && current. object instanceof ExtensionAware ) {
138
+ def extension = current. object. extensions. findByName(methodName)
139
+ if (extension != null ) {
140
+ return new TypeInformation (extension)
141
+ }
142
+ }
143
+ return null ;
144
+ }
145
+
146
+ private List<Class > findClassInScope(String name) {
147
+ if (this . projectDefaultImports == null ) {
148
+ this . projectDefaultImports = project. services. get(ImportsReader . class). getSimpleNameToFullClassNamesMapping()
149
+ }
150
+ return this . projectDefaultImports. get(name);
151
+ }
152
+
153
+ @Nullable
154
+ private Class findSuitableClass(String className, Class parentClass) {
155
+ def candidates = (findClassInScope(className) ?: []) + [className]
156
+ for (String candidate in candidates) {
157
+ try {
158
+ def candidateClass = Class . forName(candidate)
159
+ if (parentClass. isAssignableFrom(candidateClass)) {
160
+ return candidateClass
161
+ }
162
+ } catch (ignored) {
163
+ // ignore and try the next candidate
164
+ }
165
+ }
166
+ return null
167
+ }
168
+
169
+ @Nullable
170
+ private Class extractTaskType(MethodCallExpression currentExpression) {
171
+ for (Expression arg in currentExpression. arguments) {
172
+ if (arg instanceof VariableExpression || arg instanceof ConstantExpression ) {
173
+ def candidate = findSuitableClass(arg. text, Task . class)
174
+ if (candidate != null ) {
175
+ return candidate
176
+ }
177
+ } else if (arg instanceof MapExpression ) {
178
+ def type = arg
179
+ .mapEntryExpressions
180
+ .find { it. keyExpression. text == " type" }
181
+ ? . valueExpression?. text
182
+ if (type != null ) {
183
+ def candidate = findSuitableClass(type, Task . class)
184
+ if (candidate != null ) {
185
+ return candidate
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return null
191
+ }
192
+ }
193
+
194
+ class TypeInformation {
195
+ @Nullable
196
+ Class clazz
197
+ @Nullable
198
+ Object object
199
+
200
+ TypeInformation (Object object ) {
201
+ this . object = object
202
+ this . clazz = object. class
203
+ }
204
+
205
+ TypeInformation (Object object , Class clazz ) {
206
+ this . object = object
207
+ this . clazz = clazz
208
+ }
27
209
}
0 commit comments