mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GP-1808: Added 'Run to Address'-type actions to right-click menu for some connectors.
This commit is contained in:
parent
44d7c4f031
commit
bde529b4d5
39 changed files with 1663 additions and 136 deletions
|
@ -15,16 +15,30 @@
|
|||
*/
|
||||
package ghidra.dbg.target;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.reflect.TypeUtils;
|
||||
|
||||
import ghidra.dbg.DebuggerTargetObjectIface;
|
||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
||||
import ghidra.dbg.agent.DefaultTargetObject;
|
||||
import ghidra.dbg.error.DebuggerIllegalArgumentException;
|
||||
import ghidra.dbg.target.TargetMethod.*;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap.EmptyTargetParameterMap;
|
||||
import ghidra.dbg.target.TargetMethod.TargetParameterMap.ImmutableTargetParameterMap;
|
||||
import ghidra.dbg.target.schema.TargetAttributeType;
|
||||
import ghidra.dbg.util.CollectionUtils.AbstractEmptyMap;
|
||||
import ghidra.dbg.util.CollectionUtils.AbstractNMap;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* An object which can be invoked as a method
|
||||
|
@ -38,11 +52,207 @@ public interface TargetMethod extends TargetObject {
|
|||
String RETURN_TYPE_ATTRIBUTE_NAME = PREFIX_INVISIBLE + "return_type";
|
||||
public static String REDIRECT = "<=";
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
@interface Export {
|
||||
String value();
|
||||
}
|
||||
|
||||
interface Value<T> {
|
||||
boolean specified();
|
||||
|
||||
T value();
|
||||
}
|
||||
|
||||
@interface BoolValue {
|
||||
boolean specified() default true;
|
||||
|
||||
boolean value();
|
||||
|
||||
record Val(BoolValue v) implements Value<Boolean> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface IntValue {
|
||||
boolean specified() default true;
|
||||
|
||||
int value();
|
||||
|
||||
record Val(IntValue v) implements Value<Integer> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface LongValue {
|
||||
boolean specified() default true;
|
||||
|
||||
long value();
|
||||
|
||||
record Val(LongValue v) implements Value<Long> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface FloatValue {
|
||||
boolean specified() default true;
|
||||
|
||||
float value();
|
||||
|
||||
record Val(FloatValue v) implements Value<Float> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface DoubleValue {
|
||||
boolean specified() default true;
|
||||
|
||||
double value();
|
||||
|
||||
record Val(DoubleValue v) implements Value<Double> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface BytesValue {
|
||||
boolean specified() default true;
|
||||
|
||||
byte[] value();
|
||||
|
||||
record Val(BytesValue v) implements Value<byte[]> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface StringValue {
|
||||
boolean specified() default true;
|
||||
|
||||
String value();
|
||||
|
||||
record Val(StringValue v) implements Value<String> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String value() {
|
||||
return v.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface StringsValue {
|
||||
boolean specified() default true;
|
||||
|
||||
String[] value();
|
||||
|
||||
record Val(StringsValue v) implements Value<List<String>> {
|
||||
@Override
|
||||
public boolean specified() {
|
||||
return v.specified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> value() {
|
||||
return List.of(v.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Address, Range, BreakKind[Set], etc?
|
||||
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Param {
|
||||
List<Function<Param, Value<?>>> DEFAULTS = List.of(
|
||||
p -> new BoolValue.Val(p.defaultBool()),
|
||||
p -> new IntValue.Val(p.defaultInt()),
|
||||
p -> new LongValue.Val(p.defaultLong()),
|
||||
p -> new FloatValue.Val(p.defaultFloat()),
|
||||
p -> new DoubleValue.Val(p.defaultDouble()),
|
||||
p -> new BytesValue.Val(p.defaultBytes()),
|
||||
p -> new StringValue.Val(p.defaultString()));
|
||||
|
||||
String name();
|
||||
|
||||
String display();
|
||||
|
||||
String description();
|
||||
|
||||
// TODO: Something that hints at changes in activation?
|
||||
|
||||
boolean required() default true;
|
||||
|
||||
BoolValue defaultBool() default @BoolValue(specified = false, value = false);
|
||||
|
||||
IntValue defaultInt() default @IntValue(specified = false, value = 0);
|
||||
|
||||
LongValue defaultLong() default @LongValue(specified = false, value = 0);
|
||||
|
||||
FloatValue defaultFloat() default @FloatValue(specified = false, value = 0);
|
||||
|
||||
DoubleValue defaultDouble() default @DoubleValue(specified = false, value = 0);
|
||||
|
||||
BytesValue defaultBytes() default @BytesValue(specified = false, value = {});
|
||||
|
||||
StringValue defaultString() default @StringValue(specified = false, value = "");
|
||||
|
||||
StringsValue choicesString() default @StringsValue(specified = false, value = {});
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of a method parameter
|
||||
*
|
||||
* <p>
|
||||
* TODO: For convenience, these should be programmable via annotations.
|
||||
* <P>
|
||||
* TODO: Should this be incorporated into schemas?
|
||||
*
|
||||
|
@ -85,6 +295,83 @@ public interface TargetMethod extends TargetObject {
|
|||
choices);
|
||||
}
|
||||
|
||||
protected static boolean isRequired(Class<?> type, Param param) {
|
||||
if (!type.isPrimitive()) {
|
||||
return param.required();
|
||||
}
|
||||
if (type == boolean.class) {
|
||||
return !param.defaultBool().specified();
|
||||
}
|
||||
if (type == int.class) {
|
||||
return !param.defaultInt().specified();
|
||||
}
|
||||
if (type == long.class) {
|
||||
return !param.defaultLong().specified();
|
||||
}
|
||||
if (type == float.class) {
|
||||
return !param.defaultFloat().specified();
|
||||
}
|
||||
if (type == double.class) {
|
||||
return !param.defaultDouble().specified();
|
||||
}
|
||||
throw new IllegalArgumentException("Parameter type not allowed: " + type);
|
||||
}
|
||||
|
||||
protected static Object getDefault(Param annot) {
|
||||
List<Object> defaults = new ArrayList<>();
|
||||
for (Function<Param, Value<?>> df : Param.DEFAULTS) {
|
||||
Value<?> value = df.apply(annot);
|
||||
if (value.specified()) {
|
||||
defaults.add(value.value());
|
||||
}
|
||||
}
|
||||
if (defaults.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (defaults.size() > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can only specify one default value. Got " + defaults);
|
||||
}
|
||||
return defaults.get(0);
|
||||
}
|
||||
|
||||
protected static <T> T getDefault(Class<T> type, Param annot) {
|
||||
Object dv = getDefault(annot);
|
||||
if (dv == null) {
|
||||
return null;
|
||||
}
|
||||
if (!type.isInstance(dv)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Type of default does not match that of parameter. Expected type " + type +
|
||||
". Got (" + dv.getClass() + ")" + dv);
|
||||
}
|
||||
return type.cast(dv);
|
||||
}
|
||||
|
||||
protected static <T> ParameterDescription<T> annotated(Class<T> type, Param annot) {
|
||||
boolean required = isRequired(type, annot);
|
||||
T defaultValue = getDefault(type, annot);
|
||||
return ParameterDescription.create(type, annot.name(),
|
||||
required, defaultValue, annot.display(), annot.description());
|
||||
}
|
||||
|
||||
public static ParameterDescription<?> annotated(Parameter parameter) {
|
||||
Param annot = parameter.getAnnotation(Param.class);
|
||||
if (annot == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing @" + Param.class.getSimpleName() + " on " + parameter);
|
||||
}
|
||||
if (annot.choicesString().specified()) {
|
||||
if (parameter.getType() != String.class) {
|
||||
throw new IllegalArgumentException(
|
||||
"Can only specify choices for String parameter");
|
||||
}
|
||||
return ParameterDescription.choices(String.class, annot.name(),
|
||||
List.of(annot.choicesString().value()), annot.display(), annot.description());
|
||||
}
|
||||
return annotated(MethodType.methodType(parameter.getType()).wrap().returnType(), annot);
|
||||
}
|
||||
|
||||
public final Class<T> type;
|
||||
public final String name;
|
||||
public final T defaultValue;
|
||||
|
@ -199,13 +486,78 @@ public interface TargetMethod extends TargetObject {
|
|||
public static TargetParameterMap ofEntries(
|
||||
Entry<String, ParameterDescription<?>>... entries) {
|
||||
Map<String, ParameterDescription<?>> ordered = new LinkedHashMap<>();
|
||||
for (Entry<String, ParameterDescription<?>> ent: entries) {
|
||||
for (Entry<String, ParameterDescription<?>> ent : entries) {
|
||||
ordered.put(ent.getKey(), ent.getValue());
|
||||
}
|
||||
return new ImmutableTargetParameterMap(ordered);
|
||||
}
|
||||
}
|
||||
|
||||
class AnnotatedTargetMethod extends DefaultTargetObject<TargetObject, TargetObject>
|
||||
implements TargetMethod {
|
||||
|
||||
public static Map<String, AnnotatedTargetMethod> collectExports(Lookup lookup,
|
||||
AbstractDebuggerObjectModel model, TargetObject parent) {
|
||||
Map<String, AnnotatedTargetMethod> result = new HashMap<>();
|
||||
Set<Class<?>> allClasses = new LinkedHashSet<>();
|
||||
allClasses.add(parent.getClass());
|
||||
allClasses.addAll(ReflectionUtilities.getAllParents(parent.getClass()));
|
||||
for (Class<?> declCls : allClasses) {
|
||||
for (Method method : declCls.getDeclaredMethods()) {
|
||||
Export annot = method.getAnnotation(Export.class);
|
||||
if (annot == null || result.containsKey(annot.value())) {
|
||||
continue;
|
||||
}
|
||||
result.put(annot.value(),
|
||||
new AnnotatedTargetMethod(lookup, model, parent, method, annot));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private final MethodHandle handle;
|
||||
private final TargetParameterMap params;
|
||||
|
||||
public AnnotatedTargetMethod(Lookup lookup, AbstractDebuggerObjectModel model,
|
||||
TargetObject parent, Method method, Export annot) {
|
||||
super(model, parent, annot.value(), "Method");
|
||||
try {
|
||||
this.handle = lookup.unreflect(method).bindTo(parent);
|
||||
}
|
||||
catch (IllegalAccessException e) {
|
||||
throw new IllegalArgumentException("Method " + method + " is not accessible");
|
||||
}
|
||||
this.params = TargetMethod.makeParameters(
|
||||
Stream.of(method.getParameters()).map(ParameterDescription::annotated));
|
||||
|
||||
Map<TypeVariable<?>, Type> argsCf = TypeUtils
|
||||
.getTypeArguments(method.getGenericReturnType(), CompletableFuture.class);
|
||||
Type typeCfT = argsCf.get(CompletableFuture.class.getTypeParameters()[0]);
|
||||
Class<?> returnType = TypeUtils.getRawType(typeCfT, typeCfT);
|
||||
|
||||
changeAttributes(List.of(), Map.ofEntries(
|
||||
Map.entry(RETURN_TYPE_ATTRIBUTE_NAME, returnType),
|
||||
Map.entry(PARAMETERS_ATTRIBUTE_NAME, params)),
|
||||
"Initialize");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public CompletableFuture<Object> invoke(Map<String, ?> arguments) {
|
||||
Map<String, ?> valid = TargetMethod.validateArguments(params, arguments, false);
|
||||
List<Object> args = new ArrayList<>(params.size());
|
||||
for (ParameterDescription<?> p : params.values()) {
|
||||
args.add(p.get(valid));
|
||||
}
|
||||
try {
|
||||
return (CompletableFuture<Object>) handle.invokeWithArguments(args);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a map of parameter descriptions from a stream
|
||||
*
|
||||
|
|
|
@ -66,31 +66,12 @@ public interface TargetSteppable extends TargetObject {
|
|||
}
|
||||
|
||||
enum TargetStepKind {
|
||||
/**
|
||||
* Step strictly forward
|
||||
*
|
||||
* <p>
|
||||
* To avoid runaway execution, stepping should cease if execution returns from the current
|
||||
* frame.
|
||||
*
|
||||
* <p>
|
||||
* In more detail: step until execution reaches the instruction following this one,
|
||||
* regardless of the current frame. This differs from {@link #UNTIL} in that it doesn't
|
||||
* regard the current frame.
|
||||
*/
|
||||
ADVANCE,
|
||||
/**
|
||||
* Step out of the current function.
|
||||
*
|
||||
* <p>
|
||||
* In more detail: step until the object has executed the return instruction that returns
|
||||
* from the current frame.
|
||||
*
|
||||
* <p>
|
||||
* TODO: This step is geared toward GDB's {@code advance}, which actually takes a parameter.
|
||||
* Perhaps this API should adjust to accommodate stepping parameters. Would probably want a
|
||||
* strict set of forms, though, and a given kind should have the same form everywhere. If we
|
||||
* do that, then we could do nifty pop-up actions, like "Step: Advance to here".
|
||||
*/
|
||||
FINISH,
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.apache.commons.lang3.reflect.TypeUtils;
|
||||
|
||||
import ghidra.dbg.DebuggerTargetObjectIface;
|
||||
import ghidra.dbg.target.TargetMethod;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
||||
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema.MinimalSchemaContext;
|
||||
|
@ -198,6 +199,51 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
|
|||
}
|
||||
}
|
||||
|
||||
protected void addExportedTargetMethodsFromClass(SchemaBuilder builder,
|
||||
Class<? extends TargetObject> declCls, Class<? extends TargetObject> cls) {
|
||||
for (Method declMethod : declCls.getDeclaredMethods()) {
|
||||
int mod = declMethod.getModifiers();
|
||||
if (!Modifier.isPublic(mod)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TargetMethod.Export export = declMethod.getAnnotation(TargetMethod.Export.class);
|
||||
if (export == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeSchema exists = builder.getAttributeSchema(export.value());
|
||||
if (exists != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SchemaName snMethod = new SchemaName("Method");
|
||||
if (getSchemaOrNull(snMethod) == null) {
|
||||
builder(snMethod)
|
||||
.addInterface(TargetMethod.class)
|
||||
.setDefaultElementSchema(EnumerableTargetObjectSchema.VOID.getName())
|
||||
.addAttributeSchema(
|
||||
new DefaultAttributeSchema(TargetObject.DISPLAY_ATTRIBUTE_NAME,
|
||||
EnumerableTargetObjectSchema.STRING.getName(), true, true, true),
|
||||
"default")
|
||||
.addAttributeSchema(
|
||||
new DefaultAttributeSchema(TargetMethod.RETURN_TYPE_ATTRIBUTE_NAME,
|
||||
EnumerableTargetObjectSchema.TYPE.getName(), true, true, true),
|
||||
"default")
|
||||
.addAttributeSchema(
|
||||
new DefaultAttributeSchema(TargetMethod.PARAMETERS_ATTRIBUTE_NAME,
|
||||
EnumerableTargetObjectSchema.MAP_PARAMETERS.getName(), true, true,
|
||||
true),
|
||||
"default")
|
||||
.setDefaultAttributeSchema(AttributeSchema.DEFAULT_VOID)
|
||||
.buildAndAdd();
|
||||
}
|
||||
|
||||
builder.addAttributeSchema(
|
||||
new DefaultAttributeSchema(export.value(), snMethod, true, true, true), declMethod);
|
||||
}
|
||||
}
|
||||
|
||||
protected TargetObjectSchema fromAnnotatedClass(Class<? extends TargetObject> cls) {
|
||||
synchronized (namesByClass) {
|
||||
SchemaName name = nameFromAnnotatedClass(cls);
|
||||
|
@ -268,27 +314,24 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
|
|||
throw new IllegalArgumentException(
|
||||
"Could not identify unique element class (" + bounds + ") for " + cls);
|
||||
}
|
||||
else {
|
||||
Class<? extends TargetObject> bound = bounds.iterator().next();
|
||||
SchemaName schemaName;
|
||||
try {
|
||||
schemaName = nameFromClass(bound);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not get schema name from bound " + bound + " of " + cls +
|
||||
".fetchElements()",
|
||||
e);
|
||||
}
|
||||
builder.setDefaultElementSchema(schemaName);
|
||||
Class<? extends TargetObject> bound = bounds.iterator().next();
|
||||
SchemaName schemaName;
|
||||
try {
|
||||
schemaName = nameFromClass(bound);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not get schema name from bound " + bound + " of " + cls +
|
||||
".fetchElements()",
|
||||
e);
|
||||
}
|
||||
builder.setDefaultElementSchema(schemaName);
|
||||
}
|
||||
|
||||
addPublicMethodsFromClass(builder, cls, cls);
|
||||
for (Class<?> parent : allParents) {
|
||||
if (TargetObject.class.isAssignableFrom(parent)) {
|
||||
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
|
||||
cls);
|
||||
addPublicMethodsFromClass(builder, parent.asSubclass(TargetObject.class), cls);
|
||||
}
|
||||
}
|
||||
for (TargetAttributeType at : info.attributes()) {
|
||||
|
@ -299,6 +342,13 @@ public class AnnotatedSchemaContext extends DefaultSchemaContext {
|
|||
}
|
||||
builder.replaceAttributeSchema(attrSchema, at);
|
||||
}
|
||||
addExportedTargetMethodsFromClass(builder, cls, cls);
|
||||
for (Class<?> parent : allParents) {
|
||||
if (TargetObject.class.isAssignableFrom(parent)) {
|
||||
addExportedTargetMethodsFromClass(builder, parent.asSubclass(TargetObject.class),
|
||||
cls);
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,10 @@ public class DefaultSchemaContext implements SchemaContext {
|
|||
schemas.put(schema.getName(), schema);
|
||||
}
|
||||
|
||||
public synchronized void replaceSchema(TargetObjectSchema schema) {
|
||||
schemas.put(schema.getName(), schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized TargetObjectSchema getSchemaOrNull(SchemaName name) {
|
||||
return schemas.get(name);
|
||||
|
|
|
@ -24,7 +24,7 @@ public class DefaultTargetObjectSchema
|
|||
implements TargetObjectSchema, Comparable<DefaultTargetObjectSchema> {
|
||||
private static final String INDENT = " ";
|
||||
|
||||
protected static class DefaultAttributeSchema
|
||||
public static class DefaultAttributeSchema
|
||||
implements AttributeSchema, Comparable<DefaultAttributeSchema> {
|
||||
private final String name;
|
||||
private final SchemaName schema;
|
||||
|
|
|
@ -36,7 +36,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
|
|||
* <p>
|
||||
* The described value can be any primitive or a {@link TargetObject}.
|
||||
*/
|
||||
ANY("ANY", Object.class) {
|
||||
ANY(Object.class) {
|
||||
@Override
|
||||
public SchemaName getDefaultElementSchema() {
|
||||
return OBJECT.getName();
|
||||
|
@ -53,7 +53,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
|
|||
* <p>
|
||||
* This requires nothing more than the described value to be a {@link TargetObject}.
|
||||
*/
|
||||
OBJECT("OBJECT", TargetObject.class) {
|
||||
OBJECT(TargetObject.class) {
|
||||
@Override
|
||||
public SchemaName getDefaultElementSchema() {
|
||||
return OBJECT.getName();
|
||||
|
@ -64,6 +64,7 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
|
|||
return AttributeSchema.DEFAULT_ANY;
|
||||
}
|
||||
},
|
||||
TYPE(Class.class),
|
||||
/**
|
||||
* A type so restrictive nothing can satisfy it.
|
||||
*
|
||||
|
@ -72,22 +73,23 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
|
|||
* the default attribute when only certain enumerated attributes are allowed. It is also used as
|
||||
* the type for the children of primitives, since primitives cannot have successors.
|
||||
*/
|
||||
VOID("VOID", Void.class, void.class),
|
||||
BOOL("BOOL", Boolean.class, boolean.class),
|
||||
BYTE("BYTE", Byte.class, byte.class),
|
||||
SHORT("SHORT", Short.class, short.class),
|
||||
INT("INT", Integer.class, int.class),
|
||||
LONG("LONG", Long.class, long.class),
|
||||
STRING("STRING", String.class),
|
||||
ADDRESS("ADDRESS", Address.class),
|
||||
RANGE("RANGE", AddressRange.class),
|
||||
DATA_TYPE("DATA_TYPE", TargetDataType.class),
|
||||
LIST_OBJECT("LIST_OBJECT", TargetObjectList.class),
|
||||
MAP_PARAMETERS("MAP_PARAMETERS", TargetParameterMap.class),
|
||||
SET_ATTACH_KIND("SET_ATTACH_KIND", TargetAttachKindSet.class), // TODO: Limited built-in generics
|
||||
SET_BREAKPOINT_KIND("SET_BREAKPOINT_KIND", TargetBreakpointKindSet.class),
|
||||
SET_STEP_KIND("SET_STEP_KIND", TargetStepKindSet.class),
|
||||
EXECUTION_STATE("EXECUTION_STATE", TargetExecutionState.class);
|
||||
VOID(Void.class, void.class),
|
||||
BOOL(Boolean.class, boolean.class),
|
||||
BYTE(Byte.class, byte.class),
|
||||
SHORT(Short.class, short.class),
|
||||
INT(Integer.class, int.class),
|
||||
LONG(Long.class, long.class),
|
||||
STRING(String.class),
|
||||
ADDRESS(Address.class),
|
||||
RANGE(AddressRange.class),
|
||||
DATA_TYPE(TargetDataType.class),
|
||||
// TODO: Limited built-in generics?
|
||||
LIST_OBJECT(TargetObjectList.class),
|
||||
MAP_PARAMETERS(TargetParameterMap.class),
|
||||
SET_ATTACH_KIND(TargetAttachKindSet.class),
|
||||
SET_BREAKPOINT_KIND(TargetBreakpointKindSet.class),
|
||||
SET_STEP_KIND(TargetStepKindSet.class),
|
||||
EXECUTION_STATE(TargetExecutionState.class);
|
||||
|
||||
public static final class MinimalSchemaContext extends DefaultSchemaContext {
|
||||
public static final SchemaContext INSTANCE = new MinimalSchemaContext();
|
||||
|
@ -126,8 +128,8 @@ public enum EnumerableTargetObjectSchema implements TargetObjectSchema {
|
|||
private final SchemaName name;
|
||||
private final List<Class<?>> types;
|
||||
|
||||
private EnumerableTargetObjectSchema(String name, Class<?>... types) {
|
||||
this.name = new SchemaName(name);
|
||||
private EnumerableTargetObjectSchema(Class<?>... types) {
|
||||
this.name = new SchemaName(this.name());
|
||||
this.types = List.of(types);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,21 @@ public class SchemaBuilder {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
public SchemaBuilder(DefaultSchemaContext context, TargetObjectSchema schema) {
|
||||
this(context, schema.getName());
|
||||
setType(schema.getType());
|
||||
setInterfaces(schema.getInterfaces());
|
||||
setCanonicalContainer(schema.isCanonicalContainer());
|
||||
|
||||
elementSchemas.putAll(schema.getElementSchemas());
|
||||
setDefaultElementSchema(schema.getDefaultElementSchema());
|
||||
setElementResyncMode(schema.getElementResyncMode());
|
||||
|
||||
attributeSchemas.putAll(schema.getAttributeSchemas());
|
||||
setDefaultAttributeSchema(schema.getDefaultAttributeSchema());
|
||||
setAttributeResyncMode(schema.getAttributeResyncMode());
|
||||
}
|
||||
|
||||
public SchemaBuilder setType(Class<?> type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
|
|
|
@ -478,7 +478,8 @@ public interface TargetObjectSchema {
|
|||
throw new IllegalArgumentException("Must provide a specific interface");
|
||||
}
|
||||
PathMatcher result = new PathMatcher();
|
||||
Private.searchFor(this, result, prefix, true, type, requireCanonical, new HashSet<>());
|
||||
Private.searchFor(this, result, prefix, true, type, false, requireCanonical,
|
||||
new HashSet<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -610,37 +611,44 @@ public interface TargetObjectSchema {
|
|||
|
||||
private static void searchFor(TargetObjectSchema sch, PathMatcher result,
|
||||
List<String> prefix, boolean parentIsCanonical, Class<? extends TargetObject> type,
|
||||
boolean requireCanonical, Set<TargetObjectSchema> visited) {
|
||||
boolean requireAggregate, boolean requireCanonical,
|
||||
Set<TargetObjectSchema> visited) {
|
||||
if (sch instanceof EnumerableTargetObjectSchema) {
|
||||
return;
|
||||
}
|
||||
if (sch.getInterfaces().contains(type) && (parentIsCanonical || !requireCanonical)) {
|
||||
result.addPattern(prefix);
|
||||
}
|
||||
if (!visited.add(sch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sch.getInterfaces().contains(type) && parentIsCanonical) {
|
||||
result.addPattern(prefix);
|
||||
if (requireAggregate && !sch.getInterfaces().contains(TargetAggregate.class)) {
|
||||
return;
|
||||
}
|
||||
SchemaContext ctx = sch.getContext();
|
||||
boolean isCanonical = sch.isCanonicalContainer();
|
||||
for (Entry<String, SchemaName> ent : sch.getElementSchemas().entrySet()) {
|
||||
List<String> extended = PathUtils.index(prefix, ent.getKey());
|
||||
TargetObjectSchema elemSchema = ctx.getSchema(ent.getValue());
|
||||
searchFor(elemSchema, result, extended, isCanonical, type, requireCanonical,
|
||||
visited);
|
||||
searchFor(elemSchema, result, extended, isCanonical, type, requireAggregate,
|
||||
requireCanonical, visited);
|
||||
}
|
||||
List<String> deExtended = PathUtils.extend(prefix, "[]");
|
||||
TargetObjectSchema deSchema = ctx.getSchema(sch.getDefaultElementSchema());
|
||||
searchFor(deSchema, result, deExtended, isCanonical, type, requireCanonical, visited);
|
||||
searchFor(deSchema, result, deExtended, isCanonical, type, requireAggregate,
|
||||
requireCanonical, visited);
|
||||
|
||||
for (Entry<String, AttributeSchema> ent : sch.getAttributeSchemas().entrySet()) {
|
||||
List<String> extended = PathUtils.extend(prefix, ent.getKey());
|
||||
TargetObjectSchema attrSchema = ctx.getSchema(ent.getValue().getSchema());
|
||||
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireCanonical,
|
||||
visited);
|
||||
searchFor(attrSchema, result, extended, parentIsCanonical, type, requireAggregate,
|
||||
requireCanonical, visited);
|
||||
}
|
||||
List<String> daExtended = PathUtils.extend(prefix, "");
|
||||
TargetObjectSchema daSchema =
|
||||
ctx.getSchema(sch.getDefaultAttributeSchema().getSchema());
|
||||
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireCanonical,
|
||||
visited);
|
||||
searchFor(daSchema, result, daExtended, parentIsCanonical, type, requireAggregate,
|
||||
requireCanonical, visited);
|
||||
|
||||
visited.remove(sch);
|
||||
}
|
||||
|
@ -774,6 +782,34 @@ public interface TargetObjectSchema {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for all suitable objects with this schema at the given path
|
||||
*
|
||||
* <p>
|
||||
* This behaves like {@link #searchForSuitable(Class, List)}, except that it returns a matcher
|
||||
* for all possibilities. Conventionally, when the client uses the matcher to find suitable
|
||||
* objects and must choose from among the results, those having the longer paths should be
|
||||
* preferred. More specifically, it should prefer those sharing the longer path prefixes with
|
||||
* the given path. The client should <em>not</em> just take the first objects, since these will
|
||||
* likely have the shortest paths. If exactly one object is required, consider using
|
||||
* {@link #searchForSuitable(Class, List)} instead.
|
||||
*
|
||||
* @param type
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
default PathPredicates matcherForSuitable(Class<? extends TargetObject> type,
|
||||
List<String> path) {
|
||||
PathMatcher result = new PathMatcher();
|
||||
Set<TargetObjectSchema> visited = new HashSet<>();
|
||||
List<TargetObjectSchema> schemas = getSuccessorSchemas(path);
|
||||
for (; path != null; path = PathUtils.parent(path)) {
|
||||
TargetObjectSchema schema = schemas.get(path.size());
|
||||
Private.searchFor(schema, result, path, false, type, true, false, visited);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #searchForSuitable(Class, List)}, but searches for the canonical container whose
|
||||
* elements have the given type
|
||||
|
|
|
@ -327,28 +327,42 @@ public interface PathPredicates {
|
|||
}
|
||||
if (successorCouldMatch(path, true)) {
|
||||
Set<String> nextNames = getNextNames(path);
|
||||
if (!nextNames.isEmpty()) {
|
||||
if (nextNames.equals(PathMatcher.WILD_SINGLETON)) {
|
||||
for (Map.Entry<String, ?> ent : cur.getCachedAttributes().entrySet()) {
|
||||
Object value = ent.getValue();
|
||||
if (!(value instanceof TargetObject)) {
|
||||
if (!(ent.getValue() instanceof TargetObject obj)) {
|
||||
continue;
|
||||
}
|
||||
String name = ent.getKey();
|
||||
if (!anyMatches(nextNames, name)) {
|
||||
continue;
|
||||
}
|
||||
TargetObject obj = (TargetObject) value;
|
||||
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (String name : nextNames) {
|
||||
if (!(cur.getCachedAttribute(name) instanceof TargetObject obj)) {
|
||||
continue;
|
||||
}
|
||||
getCachedSuccessors(result, PathUtils.extend(path, name), obj);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> nextIndices = getNextIndices(path);
|
||||
if (!nextIndices.isEmpty()) {
|
||||
if (nextIndices.equals(PathMatcher.WILD_SINGLETON)) {
|
||||
for (Map.Entry<String, ? extends TargetObject> ent : cur.getCachedElements()
|
||||
.entrySet()) {
|
||||
TargetObject obj = ent.getValue();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
String index = ent.getKey();
|
||||
if (!anyMatches(nextIndices, index)) {
|
||||
continue;
|
||||
getCachedSuccessors(result, PathUtils.index(path, index), obj);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Map<String, ? extends TargetObject> elements = cur.getCachedElements();
|
||||
for (String index : nextIndices) {
|
||||
TargetObject obj = elements.get(index);
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
getCachedSuccessors(result, PathUtils.index(path, index), obj);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ public class TestDebuggerObjectModel extends EmptyDebuggerObjectModel {
|
|||
}
|
||||
|
||||
protected TestTargetSession newTestTargetSession(String rootHint) {
|
||||
return new TestTargetSession(this, rootHint, ROOT_SCHEMA);
|
||||
return new TestTargetSession(this, rootHint, getRootSchema());
|
||||
}
|
||||
|
||||
protected TestTargetEnvironment newTestTargetEnvironment(TestTargetSession session) {
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.target;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.async.AsyncTestUtils;
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.error.DebuggerIllegalArgumentException;
|
||||
import ghidra.dbg.model.*;
|
||||
import ghidra.dbg.target.TargetMethod.*;
|
||||
|
||||
public class TargetMethodTest implements AsyncTestUtils {
|
||||
@Test
|
||||
public void testAnnotatedMethodVoid0Args() throws Throwable {
|
||||
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
|
||||
@Override
|
||||
protected TestDebuggerObjectModel newModel(String typeHint) {
|
||||
return new TestDebuggerObjectModel(typeHint) {
|
||||
@Override
|
||||
protected TestTargetThread newTestTargetThread(
|
||||
TestTargetThreadContainer container, int tid) {
|
||||
return new TestTargetThread(container, tid) {
|
||||
{
|
||||
changeAttributes(List.of(),
|
||||
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
|
||||
testModel, this),
|
||||
"Methods");
|
||||
}
|
||||
|
||||
@TargetMethod.Export("MyMethod")
|
||||
public CompletableFuture<Void> myMethod() {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
mb.createTestModel();
|
||||
mb.createTestProcessesAndThreads();
|
||||
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
|
||||
assertEquals(Void.class, method.getReturnType());
|
||||
assertEquals(TargetParameterMap.of(), method.getParameters());
|
||||
assertNull(waitOn(method.invoke(Map.of())));
|
||||
|
||||
try {
|
||||
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
|
||||
fail("Didn't catch extraneous argument");
|
||||
}
|
||||
catch (DebuggerIllegalArgumentException e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodVoid1ArgBool() throws Throwable {
|
||||
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
|
||||
@Override
|
||||
protected TestDebuggerObjectModel newModel(String typeHint) {
|
||||
return new TestDebuggerObjectModel(typeHint) {
|
||||
@Override
|
||||
protected TestTargetThread newTestTargetThread(
|
||||
TestTargetThreadContainer container, int tid) {
|
||||
return new TestTargetThread(container, tid) {
|
||||
{
|
||||
changeAttributes(List.of(),
|
||||
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
|
||||
testModel, this),
|
||||
"Methods");
|
||||
}
|
||||
|
||||
@TargetMethod.Export("MyMethod")
|
||||
public CompletableFuture<Void> myMethod(
|
||||
@TargetMethod.Param(
|
||||
display = "P1",
|
||||
description = "A boolean param",
|
||||
name = "p1") boolean b) {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
mb.createTestModel();
|
||||
mb.createTestProcessesAndThreads();
|
||||
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
|
||||
assertEquals(Void.class, method.getReturnType());
|
||||
assertEquals(TargetParameterMap.ofEntries(
|
||||
Map.entry("p1",
|
||||
ParameterDescription.create(Boolean.class, "p1", true, null, "P1",
|
||||
"A boolean param"))),
|
||||
method.getParameters());
|
||||
assertNull(waitOn(method.invoke(Map.ofEntries(Map.entry("p1", true)))));
|
||||
|
||||
try {
|
||||
waitOn(method.invoke(Map.ofEntries(Map.entry("p1", "err"))));
|
||||
fail("Didn't catch type mismatch");
|
||||
}
|
||||
catch (DebuggerIllegalArgumentException e) {
|
||||
// pass
|
||||
}
|
||||
|
||||
try {
|
||||
waitOn(method.invoke(Map.ofEntries(
|
||||
Map.entry("p1", true),
|
||||
Map.entry("p2", "err"))));
|
||||
fail("Didn't catch extraneous argument");
|
||||
}
|
||||
catch (DebuggerIllegalArgumentException e) {
|
||||
// pass
|
||||
}
|
||||
|
||||
try {
|
||||
waitOn(method.invoke(Map.ofEntries()));
|
||||
fail("Didn't catch missing argument");
|
||||
}
|
||||
catch (DebuggerIllegalArgumentException e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodString1ArgInt() throws Throwable {
|
||||
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
|
||||
@Override
|
||||
protected TestDebuggerObjectModel newModel(String typeHint) {
|
||||
return new TestDebuggerObjectModel(typeHint) {
|
||||
@Override
|
||||
protected TestTargetThread newTestTargetThread(
|
||||
TestTargetThreadContainer container, int tid) {
|
||||
return new TestTargetThread(container, tid) {
|
||||
{
|
||||
changeAttributes(List.of(),
|
||||
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
|
||||
testModel, this),
|
||||
"Methods");
|
||||
}
|
||||
|
||||
@TargetMethod.Export("MyMethod")
|
||||
public CompletableFuture<String> myMethod(
|
||||
@TargetMethod.Param(
|
||||
display = "P1",
|
||||
description = "An int param",
|
||||
name = "p1") int i) {
|
||||
return CompletableFuture.completedFuture(Integer.toString(i));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
mb.createTestModel();
|
||||
mb.createTestProcessesAndThreads();
|
||||
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
|
||||
assertEquals(String.class, method.getReturnType());
|
||||
assertEquals(TargetParameterMap.ofEntries(
|
||||
Map.entry("p1",
|
||||
ParameterDescription.create(Integer.class, "p1", true, null, "P1",
|
||||
"An int param"))),
|
||||
method.getParameters());
|
||||
assertEquals("3", waitOn(method.invoke(Map.ofEntries(Map.entry("p1", 3)))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedMethodStringManyArgs() throws Throwable {
|
||||
TestDebuggerModelBuilder mb = new TestDebuggerModelBuilder() {
|
||||
@Override
|
||||
protected TestDebuggerObjectModel newModel(String typeHint) {
|
||||
return new TestDebuggerObjectModel(typeHint) {
|
||||
@Override
|
||||
protected TestTargetThread newTestTargetThread(
|
||||
TestTargetThreadContainer container, int tid) {
|
||||
return new TestTargetThread(container, tid) {
|
||||
{
|
||||
changeAttributes(List.of(),
|
||||
AnnotatedTargetMethod.collectExports(MethodHandles.lookup(),
|
||||
testModel, this),
|
||||
"Methods");
|
||||
}
|
||||
|
||||
@TargetMethod.Export("MyMethod")
|
||||
public CompletableFuture<String> myMethod(
|
||||
@TargetMethod.Param(
|
||||
display = "I",
|
||||
description = "An int param",
|
||||
name = "i") int i,
|
||||
@TargetMethod.Param(
|
||||
display = "B",
|
||||
description = "A boolean param",
|
||||
name = "b") boolean b,
|
||||
@TargetMethod.Param(
|
||||
display = "S",
|
||||
description = "A string param",
|
||||
name = "s") String s,
|
||||
@TargetMethod.Param(
|
||||
display = "L",
|
||||
description = "A long param",
|
||||
name = "l") long l) {
|
||||
return CompletableFuture
|
||||
.completedFuture(i + "," + b + "," + s + "," + l);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
mb.createTestModel();
|
||||
mb.createTestProcessesAndThreads();
|
||||
TargetMethod method = (TargetMethod) mb.testThread1.getCachedAttribute("MyMethod");
|
||||
assertEquals(String.class, method.getReturnType());
|
||||
assertEquals(TargetParameterMap.ofEntries(
|
||||
Map.entry("i",
|
||||
ParameterDescription.create(Integer.class, "i", true, null, "I",
|
||||
"An int param")),
|
||||
Map.entry("b",
|
||||
ParameterDescription.create(Boolean.class, "b", true, null, "B",
|
||||
"A boolean param")),
|
||||
Map.entry("s",
|
||||
ParameterDescription.create(String.class, "s", true, null, "S",
|
||||
"A string param")),
|
||||
Map.entry("l",
|
||||
ParameterDescription.create(Long.class, "l", true, null, "L",
|
||||
"A long param"))),
|
||||
method.getParameters());
|
||||
assertEquals("3,true,Hello,7", waitOn(method.invoke(Map.ofEntries(
|
||||
Map.entry("b", true), Map.entry("i", 3), Map.entry("s", "Hello"),
|
||||
Map.entry("l", 7L)))));
|
||||
}
|
||||
}
|
|
@ -16,17 +16,18 @@
|
|||
package ghidra.dbg.target.schema;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.async.AsyncUtils;
|
||||
import ghidra.dbg.agent.*;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.target.schema.DefaultTargetObjectSchema.DefaultAttributeSchema;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.ResyncMode;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.*;
|
||||
|
||||
public class AnnotatedTargetObjectSchemaTest {
|
||||
|
||||
|
@ -384,4 +385,31 @@ public class AnnotatedTargetObjectSchemaTest {
|
|||
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
|
||||
ctx.getSchemaForClass(TestAnnotatedTargetRootWithListedAttrsBadType.class);
|
||||
}
|
||||
|
||||
@TargetObjectSchemaInfo
|
||||
static class TestAnnotatedTargetRootWithExportedTargetMethod extends DefaultTargetModelRoot {
|
||||
public TestAnnotatedTargetRootWithExportedTargetMethod(AbstractDebuggerObjectModel model,
|
||||
String typeHint) {
|
||||
super(model, typeHint);
|
||||
}
|
||||
|
||||
@TargetMethod.Export("MyMethod")
|
||||
public CompletableFuture<Void> myMethod() {
|
||||
return AsyncUtils.NIL;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnnotatedRootSchemaWithExportedTargetMethod() {
|
||||
AnnotatedSchemaContext ctx = new AnnotatedSchemaContext();
|
||||
TargetObjectSchema schema =
|
||||
ctx.getSchemaForClass(TestAnnotatedTargetRootWithExportedTargetMethod.class);
|
||||
|
||||
AttributeSchema methodSchema = schema.getAttributeSchema("MyMethod");
|
||||
assertEquals(
|
||||
new DefaultAttributeSchema("MyMethod", new SchemaName("Method"), true, true, true),
|
||||
methodSchema);
|
||||
assertTrue(
|
||||
ctx.getSchema(new SchemaName("Method")).getInterfaces().contains(TargetMethod.class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -411,4 +411,12 @@
|
|||
<attribute name="_order" schema="INT" hidden="yes" />
|
||||
<attribute schema="VOID" />
|
||||
</schema>
|
||||
</context>
|
||||
<schema name="Method" elementResync="NEVER" attributeResync="NEVER">
|
||||
<interface name="Method" />
|
||||
<element schema="VOID" />
|
||||
<attribute name="_display" schema="STRING" fixed="yes" hidden="yes" />
|
||||
<attribute name="_return_type" schema="TYPE" fixed="yes" hidden="yes" />
|
||||
<attribute name="_parameters" schema="MAP_PARAMETERS" fixed="yes" hidden="yes" />
|
||||
<attribute schema="VOID" />
|
||||
</schema>
|
||||
</context>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue