Skip to content

Commit 2e7753f

Browse files
authored
Closure improvements (#151)
* magic * add ls support for @DelegatesTo annotation * comment
1 parent c1a68ac commit 2e7753f

File tree

9 files changed

+181
-47
lines changed

9 files changed

+181
-47
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cleanroommc.groovyscript.core.mixin.groovy;
2+
3+
import com.cleanroommc.groovyscript.GroovyScript;
4+
import groovy.lang.Closure;
5+
import groovy.lang.GroovyObjectSupport;
6+
import org.spongepowered.asm.mixin.Mixin;
7+
import org.spongepowered.asm.mixin.injection.At;
8+
import org.spongepowered.asm.mixin.injection.Inject;
9+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
10+
11+
@Mixin(value = Closure.class, remap = false)
12+
public abstract class ClosureMixin<V> extends GroovyObjectSupport {
13+
14+
@Inject(method = "call([Ljava/lang/Object;)Ljava/lang/Object;", at = @At("HEAD"), cancellable = true)
15+
public void call(Object[] arguments, CallbackInfoReturnable<V> cir) {
16+
// redirect closure call to properly catch and log errors
17+
cir.setReturnValue(GroovyScript.getSandbox().runClosure((Closure<? extends V>) (Object) this, arguments));
18+
}
19+
}

src/main/java/com/cleanroommc/groovyscript/sandbox/ClosureHelper.java

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package com.cleanroommc.groovyscript.sandbox;
22

3-
import com.cleanroommc.groovyscript.GroovyScript;
3+
import com.cleanroommc.groovyscript.api.GroovyBlacklist;
4+
import com.cleanroommc.groovyscript.api.GroovyLog;
5+
import com.cleanroommc.groovyscript.sandbox.expand.LambdaClosure;
46
import groovy.lang.Closure;
7+
import org.codehaus.groovy.runtime.ConvertedClosure;
58
import org.jetbrains.annotations.Nullable;
69

10+
import java.lang.reflect.*;
11+
import java.util.function.Function;
12+
713
public class ClosureHelper {
814

15+
private static final Object DUMMY = new Object();
16+
private static Field h;
17+
918
@Nullable
1019
public static <T> T call(Closure<T> closure, Object... args) {
11-
return GroovyScript.getSandbox().runClosure(closure, args);
20+
return closure.call(args);
1221
}
1322

1423
@Nullable
@@ -27,4 +36,72 @@ public static <T> T call(T defaultValue, Closure<?> closure, Object... args) {
2736
}
2837
return defaultValue;
2938
}
39+
40+
/**
41+
* The code inside a closure will try to find variables in its owner by default.
42+
* This allows to add another variable holder and sets it as its primary variable holder.
43+
* After this method is called, variables will be first searched in the given environment and then in the owner object.
44+
* The owner object is usually a {@link groovy.lang.Script} instance.
45+
*
46+
* @param closure closure
47+
* @param environment variable holder
48+
* @param force force overwrite current variable holder
49+
* @return the given closure
50+
*/
51+
public static <T> Closure<T> withEnvironment(Closure<T> closure, Object environment, boolean force) {
52+
if (force || closure.getDelegate() == null) {
53+
closure.setDelegate(environment);
54+
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
55+
}
56+
return closure;
57+
}
58+
59+
@GroovyBlacklist
60+
public static <T> Closure<T> of(Function<Object[], T> function) {
61+
return of(DUMMY, function);
62+
}
63+
64+
@GroovyBlacklist
65+
public static <T> Closure<T> of(Object owner, Function<Object[], T> function) {
66+
return new LambdaClosure<>(owner, function);
67+
}
68+
69+
/**
70+
* Extracts the underlying closure from a functional interface instance.
71+
*
72+
* @param functionalInterface a functional interface like {@link Function}
73+
* @return the underlying closure or null if there is no closure
74+
*/
75+
@GroovyBlacklist
76+
@Nullable
77+
public static Closure<?> getUnderlyingClosure(Object functionalInterface) {
78+
if (!(functionalInterface instanceof Proxy)) return null; // not a closure
79+
if (h == null) {
80+
try {
81+
Method getFields = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
82+
getFields.setAccessible(true);
83+
for (Field field : (Field[]) getFields.invoke(Proxy.class, false)) {
84+
if (field.getName().equals("h")) {
85+
h = field;
86+
h.setAccessible(true);
87+
break;
88+
}
89+
}
90+
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
91+
throw new RuntimeException(e);
92+
}
93+
if (h == null) {
94+
throw new IllegalStateException("Field h not found");
95+
}
96+
}
97+
try {
98+
InvocationHandler handler = (InvocationHandler) h.get(functionalInterface);
99+
if (handler instanceof ConvertedClosure convertedClosure) {
100+
return (Closure<?>) convertedClosure.getDelegate();
101+
}
102+
} catch (IllegalArgumentException | IllegalAccessException e) {
103+
GroovyLog.get().exception(e);
104+
}
105+
return null;
106+
}
30107
}

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ public void log(GroovyLog.Msg msg) {
102102
// has no sub messages -> log in a single line
103103
writeLogLine(formatLine(level, main));
104104
if (msg.shouldLogToMc()) {
105-
logger.log(msg.getLevel(), main + " in line " + GroovyScript.getSandbox().getCurrentLine());
105+
logger.log(msg.getLevel(), main);
106106
}
107107
} else if (messages.size() == 1 && main.length() + messages.get(0).length() < 100) {
108108
// has one sub message and the main message and the sub message have less than 100 characters ->
109109
// log in a single line
110110
writeLogLine(formatLine(level, main + ": - " + messages.get(0)));
111111
if (msg.shouldLogToMc()) {
112-
logger.log(msg.getLevel(), main + ": - " + messages.get(0) + " in line " + GroovyScript.getSandbox().getCurrentLine());
112+
logger.log(msg.getLevel(), main + ": - " + messages.get(0));
113113
}
114114
} else {
115115
// has multiple log lines or the main message and the first sub message are to long ->
@@ -119,7 +119,7 @@ public void log(GroovyLog.Msg msg) {
119119
writeLogLine(formatLine(level, " - " + message));
120120
}
121121
if (msg.shouldLogToMc()) {
122-
logger.log(msg.getLevel(), main + " in line " + GroovyScript.getSandbox().getCurrentLine() + " : - ");
122+
logger.log(msg.getLevel(), main + ": ");
123123
for (String message : messages) {
124124
logger.log(msg.getLevel(), " - " + message);
125125
}

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,7 @@
3131
*/
3232
public abstract class GroovySandbox {
3333

34-
private static final ThreadLocal<GroovySandbox> currentSandbox = new ThreadLocal<>();
35-
// TODO
3634
private String currentScript = null;
37-
private int currentLine = -1;
38-
39-
@Nullable
40-
public static GroovySandbox getCurrentSandbox() {
41-
return currentSandbox.get();
42-
}
4335

4436
private final URL[] scriptEnvironment;
4537
private final ThreadLocal<Boolean> running = ThreadLocal.withInitial(() -> false);
@@ -82,13 +74,11 @@ protected void registerStaticImports(Class<?>... classes) {
8274
}
8375

8476
protected void startRunning() {
85-
currentSandbox.set(this);
8677
this.running.set(true);
8778
}
8879

8980
protected void stopRunning() {
9081
this.running.set(false);
91-
currentSandbox.remove();
9282
}
9383

9484
protected GroovyScriptEngine createScriptEngine() {
@@ -107,7 +97,6 @@ protected Binding createBindings() {
10797
}
10898

10999
public void load() throws Exception {
110-
currentSandbox.set(this);
111100
preRun();
112101

113102
GroovyScriptEngine engine = createScriptEngine();
@@ -120,7 +109,6 @@ public void load() throws Exception {
120109
} finally {
121110
running.set(false);
122111
postRun();
123-
currentSandbox.set(null);
124112
setCurrentScript(null);
125113
}
126114
}
@@ -234,13 +222,8 @@ public String getCurrentScript() {
234222
return currentScript;
235223
}
236224

237-
public int getCurrentLine() {
238-
return currentLine;
239-
}
240-
241225
protected void setCurrentScript(String currentScript) {
242226
this.currentScript = currentScript;
243-
this.currentLine = -1;
244227
}
245228

246229
public static String getRelativePath(String source) {

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cleanroommc.groovyscript.sandbox;
22

33
import com.cleanroommc.groovyscript.GroovyScript;
4+
import com.cleanroommc.groovyscript.api.GroovyBlacklist;
45
import com.cleanroommc.groovyscript.api.GroovyLog;
56
import com.cleanroommc.groovyscript.compat.mods.ModSupport;
67
import com.cleanroommc.groovyscript.event.GroovyEventManager;
@@ -13,10 +14,7 @@
1314
import com.google.gson.JsonArray;
1415
import com.google.gson.JsonElement;
1516
import com.google.gson.JsonObject;
16-
import groovy.lang.Binding;
17-
import groovy.lang.Closure;
18-
import groovy.lang.GroovyClassLoader;
19-
import groovy.lang.Script;
17+
import groovy.lang.*;
2018
import groovy.util.GroovyScriptEngine;
2119
import groovy.util.ResourceException;
2220
import groovy.util.ScriptException;
@@ -25,9 +23,11 @@
2523
import net.minecraft.util.math.MathHelper;
2624
import net.minecraftforge.common.MinecraftForge;
2725
import org.apache.commons.io.FileUtils;
26+
import org.apache.groovy.internal.util.UncheckedThrow;
2827
import org.codehaus.groovy.control.CompilerConfiguration;
2928
import org.codehaus.groovy.control.SourceUnit;
3029
import org.codehaus.groovy.control.customizers.ImportCustomizer;
30+
import org.codehaus.groovy.runtime.InvokerInvocationException;
3131
import org.jetbrains.annotations.ApiStatus;
3232
import org.jetbrains.annotations.Nullable;
3333

@@ -174,12 +174,13 @@ public void load() throws Exception {
174174
throw new UnsupportedOperationException("Use run(Loader loader) instead!");
175175
}
176176

177+
@ApiStatus.Internal
177178
@Override
178179
public <T> T runClosure(Closure<T> closure, Object... args) {
179180
startRunning();
180181
T result = null;
181182
try {
182-
result = closure.call(args);
183+
result = runClosureInternal(closure, args);
183184
} catch (Throwable t) {
184185
this.storedExceptions.computeIfAbsent(Arrays.asList(t.getStackTrace()), k -> {
185186
GroovyLog.get().error("An exception occurred while running a closure!");
@@ -192,6 +193,24 @@ public <T> T runClosure(Closure<T> closure, Object... args) {
192193
return result;
193194
}
194195

196+
@GroovyBlacklist
197+
private static <T> T runClosureInternal(Closure<T> closure, Object[] args) {
198+
// original Closure.call(Object... arguments) code
199+
try {
200+
//noinspection unchecked
201+
return (T) closure.getMetaClass().invokeMethod(closure, "doCall", args);
202+
} catch (InvokerInvocationException e) {
203+
UncheckedThrow.rethrow(e.getCause());
204+
return null; // unreachable statement
205+
} catch (Exception e) {
206+
if (e instanceof RuntimeException) {
207+
throw e;
208+
} else {
209+
throw new GroovyRuntimeException(e.getMessage(), e);
210+
}
211+
}
212+
}
213+
195214
private static String mainClassName(String name) {
196215
return name.contains("$") ? name.split("\\$", 2)[0] : name;
197216
}

src/main/java/com/cleanroommc/groovyscript/sandbox/expand/ExpansionHelper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cleanroommc.groovyscript.sandbox.expand;
22

33
import com.cleanroommc.groovyscript.api.GroovyBlacklist;
4+
import com.cleanroommc.groovyscript.sandbox.ClosureHelper;
45
import groovy.lang.*;
56
import groovy.transform.Internal;
67
import org.codehaus.groovy.reflection.*;
@@ -11,6 +12,7 @@
1112
import java.lang.reflect.Method;
1213
import java.lang.reflect.Modifier;
1314
import java.util.Collection;
15+
import java.util.function.Function;
1416

1517
public class ExpansionHelper {
1618

@@ -116,9 +118,9 @@ public static void mixinMethod(Class<?> self, Class<?> other, String methodName)
116118
}
117119
}
118120

119-
public static void mixinMethod(Class<?> self, String name, LambdaClosure.AnyFunction<?> function) {
121+
public static void mixinMethod(Class<?> self, String name, Function<Object[], ?> function) {
120122
ExpandoMetaClass emc = getExpandoClass(self);
121-
emc.registerInstanceMethod(name, new LambdaClosure<>(function));
123+
emc.registerInstanceMethod(name, ClosureHelper.of(function));
122124
}
123125

124126
private static void mixinMethod(ExpandoMetaClass self, CachedMethod method, MixinInMetaClass mixin) {

src/main/java/com/cleanroommc/groovyscript/sandbox/expand/LambdaClosure.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,22 @@
22

33
import groovy.lang.Closure;
44

5+
import java.util.function.Function;
6+
57
public class LambdaClosure<T> extends Closure<T> {
68

7-
private final AnyFunction<T> function;
9+
private final Function<Object[], T> function;
810

9-
public LambdaClosure(Object owner, AnyFunction<T> function) {
11+
public LambdaClosure(Object owner, Function<Object[], T> function) {
1012
super(owner);
1113
this.function = function;
1214
}
1315

14-
public LambdaClosure(AnyFunction<T> function) {
16+
public LambdaClosure(Function<Object[], T> function) {
1517
this(function.getClass(), function);
1618
}
1719

1820
public T doCall(Object[] args) {
19-
return function.run(args);
20-
}
21-
22-
public interface AnyFunction<T> {
23-
24-
T run(Object[] args);
21+
return function.apply(args);
2522
}
2623
}

0 commit comments

Comments
 (0)