Skip to content

Commit 03905f8

Browse files
committed
Add dynamic (duck) type resolution to Painless static types (#78575)
This change adds dynamic (duck) type resolution to Painless static types using an annotation ( at dynamic_type ) to control which static types are allowed to be dynamically invoked. This annotation does not chain so any sub classes that also require dynamic type resolution must be annotated as well.
1 parent 1ba4f8f commit 03905f8

File tree

11 files changed

+327
-37
lines changed

11 files changed

+327
-37
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless.spi.annotation;
10+
11+
public class DynamicTypeAnnotation {
12+
13+
public static final String NAME = "dynamic_type";
14+
15+
public static final DynamicTypeAnnotation INSTANCE = new DynamicTypeAnnotation();
16+
17+
private DynamicTypeAnnotation() {
18+
19+
}
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless.spi.annotation;
10+
11+
import java.util.Map;
12+
13+
public class DynamicTypeAnnotationParser implements WhitelistAnnotationParser {
14+
15+
public static final DynamicTypeAnnotationParser INSTANCE = new DynamicTypeAnnotationParser();
16+
17+
private DynamicTypeAnnotationParser() {}
18+
19+
@Override
20+
public Object parse(Map<String, String> arguments) {
21+
if (arguments.isEmpty() == false) {
22+
throw new IllegalArgumentException(
23+
"unexpected parameters for [@" + DynamicTypeAnnotation.NAME + "] annotation, found " + arguments
24+
);
25+
}
26+
27+
return DynamicTypeAnnotation.INSTANCE;
28+
}
29+
}

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/annotation/WhitelistAnnotationParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public interface WhitelistAnnotationParser {
2727
new AbstractMap.SimpleEntry<>(NonDeterministicAnnotation.NAME, NonDeterministicAnnotationParser.INSTANCE),
2828
new AbstractMap.SimpleEntry<>(InjectConstantAnnotation.NAME, InjectConstantAnnotationParser.INSTANCE),
2929
new AbstractMap.SimpleEntry<>(CompileTimeOnlyAnnotation.NAME, CompileTimeOnlyAnnotationParser.INSTANCE),
30-
new AbstractMap.SimpleEntry<>(AugmentedAnnotation.NAME, AugmentedAnnotationParser.INSTANCE)
30+
new AbstractMap.SimpleEntry<>(AugmentedAnnotation.NAME, AugmentedAnnotationParser.INSTANCE),
31+
new AbstractMap.SimpleEntry<>(DynamicTypeAnnotation.NAME, DynamicTypeAnnotationParser.INSTANCE)
3132
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
3233
);
3334

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClass.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class PainlessClass {
2222
public final Map<String, PainlessField> staticFields;
2323
public final Map<String, PainlessField> fields;
2424
public final PainlessMethod functionalInterfaceMethod;
25+
public final Map<Class<?>, Object> annotations;
2526

2627
public final Map<String, PainlessMethod> runtimeMethods;
2728
public final Map<String, MethodHandle> getterMethodHandles;
@@ -31,6 +32,7 @@ public final class PainlessClass {
3132
Map<String, PainlessMethod> staticMethods, Map<String, PainlessMethod> methods,
3233
Map<String, PainlessField> staticFields, Map<String, PainlessField> fields,
3334
PainlessMethod functionalInterfaceMethod,
35+
Map<Class<?>, Object> annotations,
3436
Map<String, PainlessMethod> runtimeMethods,
3537
Map<String, MethodHandle> getterMethodHandles, Map<String, MethodHandle> setterMethodHandles) {
3638

@@ -40,6 +42,7 @@ public final class PainlessClass {
4042
this.staticFields = CollectionUtils.copyMap(staticFields);
4143
this.fields = CollectionUtils.copyMap(fields);
4244
this.functionalInterfaceMethod = functionalInterfaceMethod;
45+
this.annotations = annotations;
4346

4447
this.getterMethodHandles = CollectionUtils.copyMap(getterMethodHandles);
4548
this.setterMethodHandles = CollectionUtils.copyMap(setterMethodHandles);
@@ -63,11 +66,12 @@ public boolean equals(Object object) {
6366
Objects.equals(methods, that.methods) &&
6467
Objects.equals(staticFields, that.staticFields) &&
6568
Objects.equals(fields, that.fields) &&
66-
Objects.equals(functionalInterfaceMethod, that.functionalInterfaceMethod);
69+
Objects.equals(functionalInterfaceMethod, that.functionalInterfaceMethod) &&
70+
Objects.equals(annotations, that.annotations);
6771
}
6872

6973
@Override
7074
public int hashCode() {
71-
return Objects.hash(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod);
75+
return Objects.hash(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod, annotations);
7276
}
7377
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBuilder.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class PainlessClassBuilder {
2121
final Map<String, PainlessField> staticFields;
2222
final Map<String, PainlessField> fields;
2323
PainlessMethod functionalInterfaceMethod;
24+
final Map<Class<?>, Object> annotations;
2425

2526
final Map<String, PainlessMethod> runtimeMethods;
2627
final Map<String, MethodHandle> getterMethodHandles;
@@ -33,14 +34,15 @@ final class PainlessClassBuilder {
3334
staticFields = new HashMap<>();
3435
fields = new HashMap<>();
3536
functionalInterfaceMethod = null;
37+
annotations = new HashMap<>();
3638

3739
runtimeMethods = new HashMap<>();
3840
getterMethodHandles = new HashMap<>();
3941
setterMethodHandles = new HashMap<>();
4042
}
4143

4244
PainlessClass build() {
43-
return new PainlessClass(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod,
45+
return new PainlessClass(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod, annotations,
4446
runtimeMethods, getterMethodHandles, setterMethodHandles);
4547
}
4648

@@ -61,11 +63,12 @@ public boolean equals(Object object) {
6163
Objects.equals(methods, that.methods) &&
6264
Objects.equals(staticFields, that.staticFields) &&
6365
Objects.equals(fields, that.fields) &&
64-
Objects.equals(functionalInterfaceMethod, that.functionalInterfaceMethod);
66+
Objects.equals(functionalInterfaceMethod, that.functionalInterfaceMethod) &&
67+
Objects.equals(annotations, that.annotations);
6568
}
6669

6770
@Override
6871
public int hashCode() {
69-
return Objects.hash(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod);
72+
return Objects.hash(constructors, staticMethods, methods, staticFields, fields, functionalInterfaceMethod, annotations);
7073
}
7174
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public static PainlessLookup buildFromWhitelists(List<Whitelist> whitelists) {
118118
origin = whitelistClass.origin;
119119
painlessLookupBuilder.addPainlessClass(
120120
whitelist.classLoader, whitelistClass.javaClassName,
121-
whitelistClass.painlessAnnotations.containsKey(NoImportAnnotation.class) == false);
121+
whitelistClass.painlessAnnotations);
122122
}
123123
}
124124

@@ -236,7 +236,8 @@ private Class<?> loadClass(ClassLoader classLoader, String javaClassName, Suppli
236236
}
237237
}
238238

239-
public void addPainlessClass(ClassLoader classLoader, String javaClassName, boolean importClassName) {
239+
public void addPainlessClass(ClassLoader classLoader, String javaClassName, Map<Class<?>, Object> annotations) {
240+
240241
Objects.requireNonNull(classLoader);
241242
Objects.requireNonNull(javaClassName);
242243

@@ -255,12 +256,12 @@ public void addPainlessClass(ClassLoader classLoader, String javaClassName, bool
255256
clazz = loadClass(classLoader, javaClassName, () -> "class [" + javaClassName + "] not found");
256257
}
257258

258-
addPainlessClass(clazz, importClassName);
259+
addPainlessClass(clazz, annotations);
259260
}
260261

261-
public void addPainlessClass(Class<?> clazz, boolean importClassName) {
262+
public void addPainlessClass(Class<?> clazz, Map<Class<?>, Object> annotations) {
262263
Objects.requireNonNull(clazz);
263-
//Matcher m = new Matcher();
264+
Objects.requireNonNull(annotations);
264265

265266
if (clazz == def.class) {
266267
throw new IllegalArgumentException("cannot add reserved class [" + DEF_CLASS_NAME + "]");
@@ -296,13 +297,15 @@ public void addPainlessClass(Class<?> clazz, boolean importClassName) {
296297

297298
if (existingPainlessClassBuilder == null) {
298299
PainlessClassBuilder painlessClassBuilder = new PainlessClassBuilder();
300+
painlessClassBuilder.annotations.putAll(annotations);
299301

300302
canonicalClassNamesToClasses.put(canonicalClassName.intern(), clazz);
301303
classesToPainlessClassBuilders.put(clazz, painlessClassBuilder);
302304
}
303305

304306
String javaClassName = clazz.getName();
305307
String importedCanonicalClassName = javaClassName.substring(javaClassName.lastIndexOf('.') + 1).replace('$', '.');
308+
boolean importClassName = annotations.containsKey(NoImportAnnotation.class) == false;
306309

307310
if (canonicalClassName.equals(importedCanonicalClassName)) {
308311
if (importClassName) {

modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticAnalysisPhase.java

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.painless.lookup.PainlessConstructor;
1919
import org.elasticsearch.painless.lookup.PainlessField;
2020
import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
21+
import org.elasticsearch.painless.lookup.PainlessLookup;
2122
import org.elasticsearch.painless.lookup.PainlessLookupUtility;
2223
import org.elasticsearch.painless.lookup.PainlessMethod;
2324
import org.elasticsearch.painless.lookup.def;
@@ -69,6 +70,7 @@
6970
import org.elasticsearch.painless.node.SThrow;
7071
import org.elasticsearch.painless.node.STry;
7172
import org.elasticsearch.painless.node.SWhile;
73+
import org.elasticsearch.painless.spi.annotation.DynamicTypeAnnotation;
7274
import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation;
7375
import org.elasticsearch.painless.symbol.Decorations;
7476
import org.elasticsearch.painless.symbol.Decorations.AllEscape;
@@ -83,6 +85,7 @@
8385
import org.elasticsearch.painless.symbol.Decorations.ContinuousLoop;
8486
import org.elasticsearch.painless.symbol.Decorations.DefOptimized;
8587
import org.elasticsearch.painless.symbol.Decorations.DowncastPainlessCast;
88+
import org.elasticsearch.painless.symbol.Decorations.DynamicInvocation;
8689
import org.elasticsearch.painless.symbol.Decorations.EncodingDecoration;
8790
import org.elasticsearch.painless.symbol.Decorations.Explicit;
8891
import org.elasticsearch.painless.symbol.Decorations.ExpressionPainlessCast;
@@ -140,6 +143,7 @@
140143
import java.util.HashMap;
141144
import java.util.List;
142145
import java.util.Map;
146+
import java.util.Objects;
143147
import java.util.regex.Pattern;
144148
import java.util.regex.PatternSyntaxException;
145149

@@ -2888,9 +2892,45 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) {
28882892
"[" + semanticScope.getDecoration(userPrefixNode, PartialCanonicalTypeName.class).getPartialCanonicalTypeName() + "]"));
28892893
}
28902894

2895+
boolean dynamic = false;
2896+
PainlessMethod method = null;
2897+
2898+
if (prefixValueType != null) {
2899+
Class<?> type = prefixValueType.getValueType();
2900+
PainlessLookup lookup = semanticScope.getScriptScope().getPainlessLookup();
2901+
2902+
if (prefixValueType.getValueType() == def.class) {
2903+
dynamic = true;
2904+
} else {
2905+
method = lookup.lookupPainlessMethod(type, false, methodName, userArgumentsSize);
2906+
2907+
if (method == null) {
2908+
dynamic = lookup.lookupPainlessClass(type).annotations.containsKey(DynamicTypeAnnotation.class) &&
2909+
lookup.lookupPainlessSubClassesMethod(type, methodName, userArgumentsSize) != null;
2910+
2911+
if (dynamic == false) {
2912+
throw userCallNode.createError(new IllegalArgumentException("member method " +
2913+
"[" + prefixValueType.getValueCanonicalTypeName() + ", " + methodName + "/" + userArgumentsSize + "] " +
2914+
"not found"));
2915+
}
2916+
}
2917+
}
2918+
} else if (prefixStaticType != null) {
2919+
method = semanticScope.getScriptScope().getPainlessLookup().lookupPainlessMethod(
2920+
prefixStaticType.getStaticType(), true, methodName, userArgumentsSize);
2921+
2922+
if (method == null) {
2923+
throw userCallNode.createError(new IllegalArgumentException("static method " +
2924+
"[" + prefixStaticType.getStaticCanonicalTypeName() + ", " + methodName + "/" + userArgumentsSize + "] " +
2925+
"not found"));
2926+
}
2927+
} else {
2928+
throw userCallNode.createError(new IllegalStateException("value required: instead found no value"));
2929+
}
2930+
28912931
Class<?> valueType;
28922932

2893-
if (prefixValueType != null && prefixValueType.getValueType() == def.class) {
2933+
if (dynamic) {
28942934
for (AExpression userArgumentNode : userArgumentNodes) {
28952935
semanticScope.setCondition(userArgumentNode, Read.class);
28962936
semanticScope.setCondition(userArgumentNode, Internal.class);
@@ -2907,31 +2947,10 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) {
29072947
// TODO: remove ZonedDateTime exception when JodaCompatibleDateTime is removed
29082948
valueType = targetType == null || targetType.getTargetType() == ZonedDateTime.class ||
29092949
semanticScope.getCondition(userCallNode, Explicit.class) ? def.class : targetType.getTargetType();
2910-
} else {
2911-
PainlessMethod method;
2912-
2913-
if (prefixValueType != null) {
2914-
method = semanticScope.getScriptScope().getPainlessLookup().lookupPainlessMethod(
2915-
prefixValueType.getValueType(), false, methodName, userArgumentsSize);
2916-
2917-
if (method == null) {
2918-
throw userCallNode.createError(new IllegalArgumentException("member method " +
2919-
"[" + prefixValueType.getValueCanonicalTypeName() + ", " + methodName + "/" + userArgumentsSize + "] " +
2920-
"not found"));
2921-
}
2922-
} else if (prefixStaticType != null) {
2923-
method = semanticScope.getScriptScope().getPainlessLookup().lookupPainlessMethod(
2924-
prefixStaticType.getStaticType(), true, methodName, userArgumentsSize);
2925-
2926-
if (method == null) {
2927-
throw userCallNode.createError(new IllegalArgumentException("static method " +
2928-
"[" + prefixStaticType.getStaticCanonicalTypeName() + ", " + methodName + "/" + userArgumentsSize + "] " +
2929-
"not found"));
2930-
}
2931-
} else {
2932-
throw userCallNode.createError(new IllegalStateException("value required: instead found no value"));
2933-
}
29342950

2951+
semanticScope.setCondition(userCallNode, DynamicInvocation.class);
2952+
} else {
2953+
Objects.requireNonNull(method);
29352954
semanticScope.getScriptScope().markNonDeterministic(method.annotations.containsKey(NonDeterministicAnnotation.class));
29362955

29372956
for (int argument = 0; argument < userArgumentsSize; ++argument) {

modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
import org.elasticsearch.painless.symbol.Decorations.CompoundType;
155155
import org.elasticsearch.painless.symbol.Decorations.ContinuousLoop;
156156
import org.elasticsearch.painless.symbol.Decorations.DowncastPainlessCast;
157+
import org.elasticsearch.painless.symbol.Decorations.DynamicInvocation;
157158
import org.elasticsearch.painless.symbol.Decorations.EncodingDecoration;
158159
import org.elasticsearch.painless.symbol.Decorations.Explicit;
159160
import org.elasticsearch.painless.symbol.Decorations.ExpressionPainlessCast;
@@ -1802,7 +1803,7 @@ public void visitCall(ECall userCallNode, ScriptScope scriptScope) {
18021803
ValueType prefixValueType = scriptScope.getDecoration(userCallNode.getPrefixNode(), ValueType.class);
18031804
Class<?> valueType = scriptScope.getDecoration(userCallNode, ValueType.class).getValueType();
18041805

1805-
if (prefixValueType != null && prefixValueType.getValueType() == def.class) {
1806+
if (scriptScope.getCondition(userCallNode, DynamicInvocation.class)) {
18061807
InvokeCallDefNode irCallSubDefNode = new InvokeCallDefNode(userCallNode.getLocation());
18071808

18081809
for (AExpression userArgumentNode : userCallNode.getArgumentNodes()) {

modules/lang-painless/src/main/java/org/elasticsearch/painless/symbol/Decorations.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ public PainlessMethod getStandardPainlessMethod() {
365365
}
366366
}
367367

368+
public interface DynamicInvocation extends Condition {
369+
370+
}
371+
368372
public static class GetterPainlessMethod implements Decoration {
369373

370374
private final PainlessMethod getterPainlessMethod;

0 commit comments

Comments
 (0)