Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ch.njol.skript.SkriptAPIException;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import org.jetbrains.annotations.*;
import com.google.common.collect.ImmutableSet;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
Expand All @@ -14,6 +15,7 @@

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand All @@ -40,7 +42,7 @@ public static FunctionRegistry getRegistry() {
* The pattern for a valid function name.
* Functions must start with a letter or underscore and can only contain letters, numbers, and underscores.
*/
final static String FUNCTION_NAME_PATTERN = "[\\p{IsAlphabetic}_][\\p{IsAlphabetic}\\d_]*";
final static Pattern FUNCTION_NAME_PATTERN = Pattern.compile("[A-z_][A-z_0-9]*");

/**
* The namespace for registered global functions.
Expand Down Expand Up @@ -148,7 +150,7 @@ public void register(@Nullable String namespace, @NotNull Function<?> function)
Skript.debug("Registering function '%s'", function.getName());

String name = function.getName();
if (!name.matches(FUNCTION_NAME_PATTERN)) {
if (!FUNCTION_NAME_PATTERN.matcher(name).matches()) {
throw new SkriptAPIException("Invalid function name '" + name + "'");
}

Expand Down Expand Up @@ -212,7 +214,7 @@ private boolean signatureExists(@NotNull NamespaceIdentifier namespace, @NotNull
* The result of attempting to retrieve a function.
* Depending on the type, a {@link Retrieval} will feature different data.
*/
enum RetrievalResult {
public enum RetrievalResult {

/**
* The specified function or signature has not been registered.
Expand Down Expand Up @@ -258,7 +260,7 @@ enum RetrievalResult {
* @param retrieved The function or signature that was found if {@code result} is {@code EXACT}.
* @param conflictingArgs The conflicting arguments if {@code result} is {@code AMBIGUOUS}.
*/
record Retrieval<T>(
public record Retrieval<T>(
@NotNull RetrievalResult result,
T retrieved,
Class<?>[][] conflictingArgs
Expand Down Expand Up @@ -407,31 +409,24 @@ Retrieval<Signature<?>> getExactSignature(
public @Unmodifiable @NotNull Set<Signature<?>> getSignatures(@Nullable String namespace, @NotNull String name) {
Preconditions.checkNotNull(name, "name cannot be null");

ImmutableSet.Builder<Signature<?>> setBuilder = ImmutableSet.builder();

// obtain all global functions of "name"
Namespace globalNamespace = namespaces.get(GLOBAL_NAMESPACE);
Set<FunctionIdentifier> globalIdentifiers = globalNamespace.identifiers.get(name);
if (globalIdentifiers != null) {
for (FunctionIdentifier identifier : globalIdentifiers) {
setBuilder.add(globalNamespace.signatures.get(identifier));
}
}
Map<FunctionIdentifier, Signature<?>> total = new HashMap<>();

// obtain all local functions of "name"
if (namespace != null) {
Namespace localNamespace = namespaces.get(new NamespaceIdentifier(namespace));
if (localNamespace != null) {
Set<FunctionIdentifier> localIdentifiers = localNamespace.identifiers.get(name);
if (localIdentifiers != null) {
for (FunctionIdentifier identifier : localIdentifiers) {
setBuilder.add(localNamespace.signatures.get(identifier));
}
}
Namespace local = namespaces.getOrDefault(new NamespaceIdentifier(namespace), new Namespace());

for (FunctionIdentifier identifier : local.identifiers.getOrDefault(name, Collections.emptySet())) {
total.putIfAbsent(identifier, local.signatures.get(identifier));
}
}

return setBuilder.build();
// obtain all global functions of "name"
Namespace global = namespaces.getOrDefault(GLOBAL_NAMESPACE, new Namespace());
for (FunctionIdentifier identifier : global.identifiers.getOrDefault(name, Collections.emptySet())) {
total.putIfAbsent(identifier, global.signatures.get(identifier));
}

return Set.copyOf(total.values());
}

/**
Expand Down Expand Up @@ -509,6 +504,9 @@ private Retrieval<Signature<?>> getSignature(@NotNull NamespaceIdentifier namesp
// make sure all types in the passed array are valid for the array parameter
Class<?> arrayType = candidate.args[0].componentType();
for (Class<?> arrayArg : provided.args) {
if (arrayArg.isArray()) {
arrayArg = arrayArg.componentType();
}
if (!Converters.converterExists(arrayArg, arrayType)) {
continue candidates;
}
Expand All @@ -534,13 +532,20 @@ private Retrieval<Signature<?>> getSignature(@NotNull NamespaceIdentifier namesp
candidateType = candidate.args[i];
}

Class<?> providedType;
if (provided.args[i].isArray()) {
providedType = provided.args[i].componentType();
} else {
providedType = provided.args[i];
}

Class<?> providedArg = provided.args[i];
if (exact) {
if (providedArg != candidateType) {
continue candidates;
}
} else {
if (!Converters.converterExists(providedArg, candidateType)) {
if (!Converters.converterExists(providedType, candidateType)) {
continue candidates;
}
}
Expand Down
251 changes: 251 additions & 0 deletions src/main/java/ch/njol/skript/sections/ExprSecFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package ch.njol.skript.sections;

import ch.njol.skript.Skript;
import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.config.SimpleNode;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Example;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.expressions.base.SectionExpression;
import ch.njol.skript.lang.*;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.function.Function;
import ch.njol.skript.lang.function.FunctionRegistry;
import ch.njol.skript.lang.function.FunctionRegistry.Retrieval;
import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult;
import ch.njol.skript.lang.function.Parameter;
import ch.njol.skript.lang.function.Signature;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.LiteralUtils;
import ch.njol.util.Kleenean;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Name("Function Section")
@Description("""
Runs a function with the specified arguments.
""")
@Example("""
local function multiply(x: number, y: number) returns number:
return {_x} * {_y}

set {_x} to function multiply with arguments:
x as 2
y as 3

broadcast "%{_x}%" # returns 6
""")
@Since("INSERT VERSION")
public class ExprSecFunction extends SectionExpression<Object> {

/**
* The pattern for a valid function name.
* Functions must start with a letter or underscore and can only contain letters, numbers, and underscores.
*/
private final static Pattern FUNCTION_NAME_PATTERN = Pattern.compile("[A-z_][A-z_0-9]*");

/**
* The pattern for an argument that can be passed in the children of this section.
*/
private static final Pattern ARGUMENT_PATTERN = Pattern.compile("(?:(?:the )?argument )?(?<name>%s) set to (?<value>.+)".formatted(FUNCTION_NAME_PATTERN.toString()));

static {
Skript.registerExpression(ExprSecFunction.class, Object.class, ExpressionType.SIMPLE, "[the] function <.+> with [the] arg[ument][s]");
}

private Function<?> function;
private LinkedHashMap<String, Expression<?>> arguments = null;

@Override
public boolean init(Expression<?>[] expressions, int pattern, Kleenean delayed, ParseResult result,
@Nullable SectionNode node, @Nullable List<TriggerItem> triggerItems) {
assert node != null;

if (node.isEmpty()) {
Skript.error("A function section must contain arguments.");
return false;
}

LinkedHashMap<String, String> args = new LinkedHashMap<>();
for (Node n : node) {
if (!(n instanceof SimpleNode) || n.getKey() == null) {
Skript.error("Invalid argument declaration for a function section: ", n.getKey());
return false;
}

Matcher matcher = ARGUMENT_PATTERN.matcher(n.getKey());
if (!matcher.matches()) {
Skript.error("Invalid argument declaration for a function section: ", n.getKey());
return false;
}

args.put(matcher.group("name"), matcher.group("value"));
}

String namespace = ParserInstance.get().getCurrentScript().getConfig().getFileName();
String name = result.regexes.get(0).group();

if (!FUNCTION_NAME_PATTERN.matcher(name).matches()) {
Skript.error("The function %s does not exist.", name);
return false;
}

// todo use FunctionParser
function = findFunction(namespace, name, args);

if (function == null || arguments == null || arguments.isEmpty()) {
doesNotExist(name, args);
return false;
}

if (function.getReturnType() == null) {
Skript.error("The function %s does not return anything.", name);
return false;
}

return true;
}

/**
* Attempts to find the function to execute given the arguments.
*
* @param namespace The current script.
* @param name The name of the function.
* @param args The passed arguments.
* @return The function given the arguments, or null if no function is found.
*/
private Function<?> findFunction(String namespace, String name, LinkedHashMap<String, String> args) {
signatures:
for (Signature<?> signature : FunctionRegistry.getRegistry().getSignatures(namespace, name)) {
LinkedHashMap<String, Expression<?>> arguments = new LinkedHashMap<>();

LinkedHashMap<String, Parameter<?>> parameters = Arrays.stream(signature.getParameters())
.collect(Collectors.toMap(Parameter::getName, p -> p, (a, b) -> b, LinkedHashMap::new));
for (Entry<String, String> entry : args.entrySet()) {
Parameter<?> parameter = parameters.get(entry.getKey());

if (parameter == null) {
continue signatures;
}

//noinspection unchecked
Expression<?> expression = LiteralUtils.defendExpression(
new SkriptParser(entry.getValue(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT)
.parseExpression(parameter.getType().getC()));

if (expression == null || LiteralUtils.hasUnparsedLiteral(expression)) {
continue signatures;
}

arguments.put(entry.getKey(), expression);
}

Class<?>[] signatureArgs = Arrays.stream(signature.getParameters())
.map(it -> {
if (it.isSingleValue()) {
return it.getType().getC();
} else {
return it.getType().getC().arrayType();
}
})
.toArray(Class<?>[]::new);

Retrieval<Function<?>> retrieval = FunctionRegistry.getRegistry().getFunction(namespace, name, signatureArgs);
if (retrieval.result() == RetrievalResult.EXACT) {
this.arguments = arguments;
return retrieval.retrieved();
}
}

return null;
}

/**
* Prints the error for when a function does not exist.
*
* @param name The function name.
* @param arguments The passed arguments to the function call.
*/
private void doesNotExist(String name, LinkedHashMap<String, String> arguments) {
StringJoiner joiner = new StringJoiner(", ");

for (Map.Entry<String, String> entry : arguments.entrySet()) {
SkriptParser parser = new SkriptParser(entry.getValue(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT);

Expression<?> expression = LiteralUtils.defendExpression(parser.parseExpression(Object.class));

if (expression == null || LiteralUtils.hasUnparsedLiteral(expression)) {
joiner.add(entry.getKey() + ": ?");
continue;
}

if (expression.isSingle()) {
joiner.add(entry.getKey() + ": " + Classes.getSuperClassInfo(expression.getReturnType()).getName().getSingular());
} else {
joiner.add(entry.getKey() + ": " + Classes.getSuperClassInfo(expression.getReturnType()).getName().getPlural());
}
}

Skript.error("The function %s(%s) does not exist.", name, joiner);
}

@Override
protected Object @Nullable [] get(Event event) {
if (function == null) {
return null;
}

Object[][] args = new Object[function.getParameters().length][];
int i = 0;
for (Parameter<?> value : function.getParameters()) {
Expression<?> expression = arguments.get(value.getName());

if (expression == null) {
return null;
}

args[i] = expression.getArray(event);
i++;
}

try {
return function.execute(args);
} finally {
function.resetReturnValue();
}
}

@Override
public boolean isSingle() {
return function.isSingle();
}

@Override
public boolean isSectionOnly() {
return true;
}

@Override
public Class<?> getReturnType() {
return function.getReturnType() != null ? function.getReturnType().getC() : null;
}

@Override
public String toString(@Nullable Event event, boolean debug) {
return new SyntaxStringBuilder(event, debug)
.append("run function")
.append(function.getName())
.append("with arguments")
.toString();
}

}
Loading
Loading