|
14 | 14 |
|
15 | 15 | package org.spockframework.idea |
16 | 16 |
|
17 | | -import com.intellij.psi.PsiVariable |
18 | | - |
19 | 17 | // 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 |
21 | 19 |
|
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)) |
23 | 22 |
|
24 | 23 | // Mock(Foo) { /* delegates to instance of type Foo */ } |
25 | 24 | // 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) |
43 | 64 | } |
44 | 65 | } |
45 | | -}) |
46 | 66 |
|
| 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 | +} |
47 | 119 |
|
48 | 120 | // From https://issues.apache.org/jira/browse/GROOVY-9510 |
49 | 121 | // Properly resolve the delegatesTo for the closures of these extensions |
|
0 commit comments