<commit_before>package com.btk5h.skriptmirror.skript;

import com.btk5h.skriptmirror.Descriptor;
import com.btk5h.skriptmirror.JavaType;
import com.btk5h.skriptmirror.LRUCache;
import com.btk5h.skriptmirror.Null;
import com.btk5h.skriptmirror.Util;

import org.bukkit.event.Event;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import ch.njol.skript.Skript;
import ch.njol.skript.classes.Changer;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.ExpressionList;
import ch.njol.skript.lang.ExpressionType;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.registrations.Converters;
import ch.njol.skript.util.Utils;
import ch.njol.util.Checker;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.iterator.ArrayIterator;

public class ExprJavaCall<T> implements Expression<T> {
  private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup();
  private static final Object[] NO_ARGS = new Object[0];
  private static final Descriptor CONSTRUCTOR_DESCRIPTOR = Descriptor.create("<init>");

  static {
    //noinspection unchecked
    Skript.registerExpression(ExprJavaCall.class, Object.class,
        ExpressionType.PATTERN_MATCHES_EVERYTHING,
        "%object%..%string%(0¦!|1¦\\([%-objects%]\\))",
        "%object%.<[\\w$.\\[\\]]+>(0¦!|1¦\\([%-objects%]\\))",
        "new %javatype%\\([%-objects%]\\)");
  }

  private enum Type {
    FIELD, METHOD, CONSTRUCTOR;

    @Override
    public String toString() {
      return name().toLowerCase();
    }
  }

  static String lastErrorMessage;
  static Throwable lastError;

  private LRUCache<Descriptor, Collection<MethodHandle>> callSiteCache = new LRUCache<>(8);

  private Expression<Object> targetArg;
  private Expression<Object> args;

  private Type type;
  private boolean isDynamic;
  private boolean suppressErrors = false;

  private Descriptor staticDescriptor;
  private Expression<String> dynamicDescriptor;

  private final ExprJavaCall<?> source;
  private final Class<? extends T>[] types;
  private final Class<T> superType;

  @SuppressWarnings("unchecked")
  public ExprJavaCall() {
    this(null, (Class<? extends T>) Object.class);
  }

  @SuppressWarnings("unchecked")
  @SafeVarargs
  private ExprJavaCall(ExprJavaCall<?> source, Class<? extends T>... types) {
    this.source = source;

    if (source != null) {
      this.targetArg = source.targetArg;
      this.args = source.args;
      this.type = source.type;
      this.suppressErrors = source.suppressErrors;
      this.staticDescriptor = source.staticDescriptor;
      this.dynamicDescriptor = source.dynamicDescriptor;
    }

    this.types = types;
    this.superType = (Class<T>) Utils.getSuperType(types);
  }

  private Collection<MethodHandle> getCallSite(Descriptor e) {
    return callSiteCache.computeIfAbsent(e, this::createCallSite);
  }

  private Collection<MethodHandle> createCallSite(Descriptor e) {
    Class<?> javaClass = e.getJavaClass();

    switch (type) {
      case FIELD:
        return Util.fields(javaClass)
            .filter(f -> f.getName().equals(e.getIdentifier()))
            .peek(f -> f.setAccessible(true))
            .flatMap(Util.propagateErrors(f -> Stream.of(
                LOOKUP.unreflectGetter(f),
                LOOKUP.unreflectSetter(f)
            )))
            .filter(Objects::nonNull)
            .limit(2)
            .collect(Collectors.toList());
      case METHOD:
        return Util.methods(javaClass)
            .filter(m -> m.getName().equals(e.getIdentifier()))
            .peek(m -> m.setAccessible(true))
            .map(Util.propagateErrors(LOOKUP::unreflect))
            .filter(Objects::nonNull)
            //.map(ExprJavaCall::asSpreader)
            .collect(Collectors.toList());
      case CONSTRUCTOR:
        return Util.constructor(javaClass)
            .peek(c -> c.setAccessible(true))
            .map(Util.propagateErrors(LOOKUP::unreflectConstructor))
            .filter(Objects::nonNull)
            //.map(ExprJavaCall::asSpreader)
            .collect(Collectors.toList());
      default:
        throw new IllegalStateException();
    }
  }

  private static MethodHandle asSpreader(MethodHandle mh) {
    int paramCount = mh.type().parameterCount();
    if (mh.isVarargsCollector()) {
      if (paramCount == 1) {
        return mh;
      }

      return mh.asSpreader(Object[].class, paramCount - 1);
    }

    return mh.asSpreader(Object[].class, paramCount);
  }

  @SuppressWarnings("unchecked")
  private T[] invoke(Object target, Object[] arguments, Descriptor baseDescriptor) {
    T returnedValue = null;

    Class<?> targetClass = Util.toClass(target);
    Descriptor descriptor = specifyDescriptor(baseDescriptor, targetClass);

    if (descriptor.getJavaClass().isAssignableFrom(targetClass)) {
      Object[] arr;
      if (target instanceof JavaType) {
        arr = new Object[arguments.length];
        System.arraycopy(arguments, 0, arr, 0, arguments.length);
      } else {
        arr = new Object[arguments.length + 1];
        arr[0] = target;
        System.arraycopy(arguments, 0, arr, 1, arguments.length);
      }

      Optional<MethodHandle> method = selectMethod(descriptor, arr);

      if (method.isPresent()) {
        MethodHandle mh = method.get();

        convertTypes(mh.type(), arr);

        try {
          returnedValue = (T) mh.invokeWithArguments(arr);
        } catch (Throwable throwable) {
          lastError = throwable;
          if (!suppressErrors) {
            Skript.warning(
                String.format("%s %s%s threw a %s: %s%n",
                    type, descriptor, optionalArgs(arguments),
                    throwable.getClass().getSimpleName(), throwable.getMessage()));
          }
        }
      } else {
        lastErrorMessage = String.format("No matching %s: %s%s",
            type, descriptor, optionalArgs(arguments));
        if (!suppressErrors) {
          Skript.warning(lastErrorMessage);
        }
      }
    } else {
      lastErrorMessage = String.format("Incompatible %s call: %s on %s",
          type, descriptor, Util.getDebugName(targetClass));
      if (!suppressErrors) {
        Skript.warning(lastErrorMessage);
      }
    }

    if (returnedValue == null) {
      return Util.newArray(superType, 0);
    }

    T converted = Converters.convert(returnedValue, types);

    if (converted == null) {
      String toClasses = Arrays.stream(types)
          .map(Util::getDebugName)
          .collect(Collectors.joining(", "));
      lastErrorMessage = String.format("%s %s%s returned %s, which could not be converted to %s",
          type, descriptor, optionalArgs(arguments), toString(returnedValue), toClasses);
      if (!suppressErrors) {
        Skript.warning(lastErrorMessage);
      }
      return Util.newArray(superType, 0);
    }

    lastErrorMessage = null;
    lastError = null;

    T[] returnArray = Util.newArray(superType, 1);
    returnArray[0] = converted;
    return returnArray;
  }

  private String optionalArgs(Object... arguments) {
    if (arguments.length == 0) {
      return "";
    }
<commit_msg>Clarify error message when a nullary method is invoked (fixes #15)<commit_after>package com.btk5h.skriptmirror.skript;

import com.btk5h.skriptmirror.Descriptor;
import com.btk5h.skriptmirror.JavaType;
import com.btk5h.skriptmirror.LRUCache;
import com.btk5h.skriptmirror.Null;
import com.btk5h.skriptmirror.Util;

import org.bukkit.event.Event;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import ch.njol.skript.Skript;
import ch.njol.skript.classes.Changer;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.ExpressionList;
import ch.njol.skript.lang.ExpressionType;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.registrations.Converters;
import ch.njol.skript.util.Utils;
import ch.njol.util.Checker;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.iterator.ArrayIterator;

public class ExprJavaCall<T> implements Expression<T> {
  private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup();
  private static final Object[] NO_ARGS = new Object[0];
  private static final Descriptor CONSTRUCTOR_DESCRIPTOR = Descriptor.create("<init>");

  static {
    //noinspection unchecked
    Skript.registerExpression(ExprJavaCall.class, Object.class,
        ExpressionType.PATTERN_MATCHES_EVERYTHING,
        "%object%..%string%(0¦!|1¦\\([%-objects%]\\))",
        "%object%.<[\\w$.\\[\\]]+>(0¦!|1¦\\([%-objects%]\\))",
        "new %javatype%\\([%-objects%]\\)");
  }

  private enum Type {
    FIELD, METHOD, CONSTRUCTOR;

    @Override
    public String toString() {
      return name().toLowerCase();
    }
  }

  static String lastErrorMessage;
  static Throwable lastError;

  private LRUCache<Descriptor, Collection<MethodHandle>> callSiteCache = new LRUCache<>(8);

  private Expression<Object> targetArg;
  private Expression<Object> args;

  private Type type;
  private boolean isDynamic;
  private boolean suppressErrors = false;

  private Descriptor staticDescriptor;
  private Expression<String> dynamicDescriptor;

  private final ExprJavaCall<?> source;
  private final Class<? extends T>[] types;
  private final Class<T> superType;

  @SuppressWarnings("unchecked")
  public ExprJavaCall() {
    this(null, (Class<? extends T>) Object.class);
  }

  @SuppressWarnings("unchecked")
  @SafeVarargs
  private ExprJavaCall(ExprJavaCall<?> source, Class<? extends T>... types) {
    this.source = source;

    if (source != null) {
      this.targetArg = source.targetArg;
      this.args = source.args;
      this.type = source.type;
      this.suppressErrors = source.suppressErrors;
      this.staticDescriptor = source.staticDescriptor;
      this.dynamicDescriptor = source.dynamicDescriptor;
    }

    this.types = types;
    this.superType = (Class<T>) Utils.getSuperType(types);
  }

  private Collection<MethodHandle> getCallSite(Descriptor e) {
    return callSiteCache.computeIfAbsent(e, this::createCallSite);
  }

  private Collection<MethodHandle> createCallSite(Descriptor e) {
    Class<?> javaClass = e.getJavaClass();

    switch (type) {
      case FIELD:
        return Util.fields(javaClass)
            .filter(f -> f.getName().equals(e.getIdentifier()))
            .peek(f -> f.setAccessible(true))
            .flatMap(Util.propagateErrors(f -> Stream.of(
                LOOKUP.unreflectGetter(f),
                LOOKUP.unreflectSetter(f)
            )))
            .filter(Objects::nonNull)
            .limit(2)
            .collect(Collectors.toList());
      case METHOD:
        return Util.methods(javaClass)
            .filter(m -> m.getName().equals(e.getIdentifier()))
            .peek(m -> m.setAccessible(true))
            .map(Util.propagateErrors(LOOKUP::unreflect))
            .filter(Objects::nonNull)
            //.map(ExprJavaCall::asSpreader)
            .collect(Collectors.toList());
      case CONSTRUCTOR:
        return Util.constructor(javaClass)
            .peek(c -> c.setAccessible(true))
            .map(Util.propagateErrors(LOOKUP::unreflectConstructor))
            .filter(Objects::nonNull)
            //.map(ExprJavaCall::asSpreader)
            .collect(Collectors.toList());
      default:
        throw new IllegalStateException();
    }
  }

  private static MethodHandle asSpreader(MethodHandle mh) {
    int paramCount = mh.type().parameterCount();
    if (mh.isVarargsCollector()) {
      if (paramCount == 1) {
        return mh;
      }

      return mh.asSpreader(Object[].class, paramCount - 1);
    }

    return mh.asSpreader(Object[].class, paramCount);
  }

  @SuppressWarnings("unchecked")
  private T[] invoke(Object target, Object[] arguments, Descriptor baseDescriptor) {
    T returnedValue = null;

    Class<?> targetClass = Util.toClass(target);
    Descriptor descriptor = specifyDescriptor(baseDescriptor, targetClass);

    if (descriptor.getJavaClass().isAssignableFrom(targetClass)) {
      Object[] arr;
      if (target instanceof JavaType) {
        arr = new Object[arguments.length];
        System.arraycopy(arguments, 0, arr, 0, arguments.length);
      } else {
        arr = new Object[arguments.length + 1];
        arr[0] = target;
        System.arraycopy(arguments, 0, arr, 1, arguments.length);
      }

      Optional<MethodHandle> method = selectMethod(descriptor, arr);

      if (method.isPresent()) {
        MethodHandle mh = method.get();

        convertTypes(mh.type(), arr);

        try {
          returnedValue = (T) mh.invokeWithArguments(arr);
        } catch (Throwable throwable) {
          lastError = throwable;
          if (!suppressErrors) {
            Skript.warning(
                String.format("%s %s%s threw a %s: %s%n",
                    type, descriptor, optionalArgs(arguments),
                    throwable.getClass().getSimpleName(), throwable.getMessage()));
          }
        }
      } else {
        lastErrorMessage = String.format("No matching %s: %s%s",
            type, descriptor, optionalArgs(arguments));
        if (!suppressErrors) {
          Skript.warning(lastErrorMessage);
        }
      }
    } else {
      lastErrorMessage = String.format("Incompatible %s call: %s on %s",
          type, descriptor, Util.getDebugName(targetClass));
      if (!suppressErrors) {
        Skript.warning(lastErrorMessage);
      }
    }

    if (returnedValue == null) {
      return Util.newArray(superType, 0);
    }

    T converted = Converters.convert(returnedValue, types);

    if (converted == null) {
      String toClasses = Arrays.stream(types)
          .map(Util::getDebugName)
          .collect(Collectors.joining(", "));
      lastErrorMessage = String.format("%s %s%s returned %s, which could not be converted to %s",
          type, descriptor, optionalArgs(arguments), toString(returnedValue), toClasses);
      if (!suppressErrors) {
        Skript.warning(lastErrorMessage);
      }
      return Util.newArray(superType, 0);
    }

    lastErrorMessage = null;
    lastError = null;

    T[] returnArray = Util.newArray(superType, 1);
    returnArray[0] = converted;
    return returnArray;
  }

  private String optionalArgs(Object... arguments) {
    if (arguments.length == 0) {
      return " called without arguments";
    }

{'fraction_non_alphanumeric': 0.12606100795755967, 'fraction_numerical': 0.003580901856763926, 'mean_word_length': 3.0258942872397223, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 59, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '530828', 'n_tokens_mistral': 4645, 'n_tokens_neox': 4516, 'n_words': 897}