Skip to content

Commit ba0e48f

Browse files
authored
Make spock.gdsl fail-safe and cover some more cases (#1783)
1 parent 8a091be commit ba0e48f

1 file changed

Lines changed: 94 additions & 22 deletions

File tree

  • spock-core/src/main/resources/org/spockframework/idea

spock-core/src/main/resources/org/spockframework/idea/spock.gdsl

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,108 @@
1414

1515
package org.spockframework.idea
1616

17-
import com.intellij.psi.PsiVariable
18-
1917
// only tested with 11.1, so play it safe and bail out on earlier version
20-
if (!supportsVersion("11.1")) return
18+
if (!supportsVersion('11.1')) return
2119

22-
def ctx = context(ctype: "spock.lang.Specification", scope: closureScope(isArg: true))
20+
// handle closures that are method arguments of methods called from a class implementing MockingApi
21+
def ctx = context(ctype: 'spock.mock.MockingApi', scope: closureScope(isArg: true))
2322

2423
// Mock(Foo) { /* delegates to instance of type Foo */ }
2524
// Foo x = Mock { /* delegates to instance of type Foo */ }
26-
contributor(ctx, {
27-
def call = enclosingCall("Mock") ?: enclosingCall("Stub") ?: enclosingCall("Spy") ?:
28-
enclosingCall("GroovyMock") ?: enclosingCall("GroovyStub") ?: enclosingCall("GroovySpy")
29-
if (call) {
30-
def method = call.bind()
31-
def clazz = method?.containingClass
32-
if (clazz?.qualName == "spock.mock.MockingApi") {
33-
def mockType = call.arguments.find { it.type?.className == "Class" }
34-
if (mockType) {
35-
def typeParameter = mockType.type.parameters[0]
36-
delegatesTo(typeParameter.resolve())
37-
return
38-
}
39-
def callContext = call.context
40-
if (callContext instanceof PsiVariable) {
41-
delegatesTo(callContext.typeGroovy.resolve())
42-
}
25+
contributor(ctx) {
26+
// the context is the closure, get the enclosing method call to only handle the mock creation methods
27+
def call = enclosingCall('Mock') ?: enclosingCall('Stub') ?: enclosingCall('Spy') ?:
28+
enclosingCall('GroovyMock') ?: enclosingCall('GroovyStub') ?: enclosingCall('GroovySpy')
29+
30+
// expect to be within a mock creation method call or abort
31+
if (!call) return
32+
// expect a method bind on the method call or abort
33+
if (!respondsToParameterless(call, 'bind')) return
34+
35+
// get a reference to the called method
36+
def calledMethod = call.bind()
37+
38+
// if the called method could not be determined, abort
39+
if (!calledMethod) return
40+
// expect the called method to have a containingClass property or abort
41+
if (!respondsToProperty(calledMethod, 'containingClass')) return
42+
43+
// get the class containing the called method
44+
def clazz = calledMethod.containingClass
45+
46+
// if the class could not be determined, abort
47+
if (!clazz) return
48+
// expect the called method to have a qualName property or abort
49+
if (!respondsToProperty(clazz, 'qualName')) return
50+
// expect the called method to be from MockingApi or abort
51+
if (clazz.qualName != 'spock.mock.MockingApi') return
52+
53+
def delegateTo = { type ->
54+
if (respondsToProperty(type, 'boxedTypeName')) {
55+
// trying to mock a primitive type, so use the boxed type
56+
delegatesTo(findClass(type.boxedTypeName))
57+
} else if (respondsToParameterless(type, 'resolve')) {
58+
// the type is resolvable, e.g. because it is a reference to an actual type
59+
// so resolve the type before using it
60+
delegatesTo(type.resolve())
61+
} else {
62+
// as fallback just use the type directly
63+
delegatesTo(type)
4364
}
4465
}
45-
})
4666

67+
// if the arguments of the call can be requested,
68+
// check for an explicit type which should win over an inferred type
69+
if (respondsToProperty(call, 'arguments')) {
70+
// find the first argument that is a Class,
71+
// this is expected to be the explicit type
72+
def explicitType = call.arguments.find {
73+
respondsToProperty(it, 'classType')
74+
&& respondsToProperty(it.classType, 'name')
75+
&& (it.classType.name == 'Class')
76+
}
77+
// if an explicit type was found, use it as delegate type if possible
78+
if (explicitType) {
79+
if (!respondsToProperty(explicitType, 'type')) return
80+
if (!respondsToProperty(explicitType.type, 'parameters')) return
81+
82+
delegateTo(explicitType.type.parameters[0])
83+
return
84+
}
85+
}
86+
87+
if (!respondsToProperty(call, 'context')) return
88+
89+
def callContext = call.context
90+
if (respondsToProperty(callContext, 'typeGroovy')) {
91+
// the call context directly has a groovy type, this usually means
92+
// it is directly a variable assignment, so use the type
93+
delegateTo(callContext.typeGroovy)
94+
} else if (respondsToProperty(callContext, 'LValue')) {
95+
// the call context does not have a groovy type, but an LValue
96+
// this for example happens when you split declaration of the mock variable
97+
// and assigning it like in `Foo foo; foo = Mock { }`.
98+
def lvalue = callContext.LValue
99+
if (respondsToParameterless(lvalue, 'resolve')) {
100+
// the type is resolvable, e.g. because it is a reference to an actual type
101+
// so resolve the type before using it
102+
lvalue = lvalue.resolve()
103+
}
104+
if (respondsToProperty(lvalue, 'typeGroovy'))
105+
delegateTo(lvalue.typeGroovy)
106+
}
107+
}
108+
109+
List<MetaMethod> respondsToParameterless(Object target, String name) {
110+
return target.respondsTo(name, [] as Object[])
111+
}
112+
113+
boolean respondsToProperty(target, String name) {
114+
def nameCapitalized = name.capitalize()
115+
return respondsToParameterless(target, "get$nameCapitalized")
116+
|| respondsToParameterless(target, "is$nameCapitalized")*.returnType == [boolean]
117+
|| target.hasProperty(name)
118+
}
47119

48120
// From https://issues.apache.org/jira/browse/GROOVY-9510
49121
// Properly resolve the delegatesTo for the closures of these extensions

0 commit comments

Comments
 (0)