Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
60 changes: 50 additions & 10 deletions src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@
import ch.njol.skript.SkriptAPIException;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.UnmodifiableView;
import org.jetbrains.annotations.*;
import org.skriptlang.skript.lang.converter.Converters;
import org.skriptlang.skript.util.Registry;

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

/**
* A registry for functions.
*/
final class FunctionRegistry implements Registry<Function<?>> {
@ApiStatus.Internal
public final class FunctionRegistry implements Registry<Function<?>> {

private static FunctionRegistry registry;

Expand All @@ -38,7 +37,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 @@ -147,7 +146,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 @@ -211,7 +210,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 @@ -257,7 +256,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 @@ -395,6 +394,37 @@ Retrieval<Signature<?>> getExactSignature(
return attempt;
}

/**
* Gets every signature with the name {@code name}.
* This includes global functions and, if {@code namespace} is not null, functions under that namespace (if valid).
* @param namespace The additional namespace to obtain signatures from.
* Usually represents the path of the script this function is registered in.
* @param name The name of the signature(s) to obtain.
* @return A list of all signatures named {@code name}.
*/
public @Unmodifiable @NotNull Set<Signature<?>> getSignatures(@Nullable String namespace, @NotNull String name) {
Preconditions.checkNotNull(name, "name cannot be null");

Map<FunctionIdentifier, Signature<?>> total = new HashMap<>();

// obtain all local functions of "name"
if (namespace != null) {
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));
}
}

// 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());
}

/**
* Gets the signature for a function with the given name and arguments.
*
Expand Down Expand Up @@ -470,6 +500,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 @@ -495,13 +528,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
224 changes: 224 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,224 @@
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.Name;
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.
""")
public class ExprSecFunction extends SectionExpression<Object> {

private static final String AMBIGUOUS_ERROR =
"Skript cannot determine which function named '%s' to call. " +
"The following functions were matched: %s. " +
"Try clarifying the type of the arguments using the 'value within' expression.";

/**
* 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("(?<name>%s) set to (?<value>.+)".formatted(FUNCTION_NAME_PATTERN.toString()));

static {
Skript.registerExpression(ExprSecFunction.class, Object.class, ExpressionType.SIMPLE, "function <.+> with argument[s]");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably just be a rework of ExprResult

}

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) {
if (node == null) {
Skript.error("A section must follow this expression.");
return false;
} else if (node.isEmpty()) {
Skript.error("A function section must contain code.");
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.".formatted(name));
return false;
}

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

if (function == null || arguments == null || arguments.isEmpty()) {
doesNotExist(name, args);
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 = new SkriptParser(entry.getValue(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT)
.parseExpression(parameter.getType().getC());

if (expression == null) {
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) {
joiner.add("?");
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) {
Object[][] args = new Object[arguments.size()][];
int i = 0;
for (Expression<?> value : arguments.values()) {
args[i] = value.getArray(event);
i++;
}

return function.execute(args);
}

@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();
}

}
34 changes: 34 additions & 0 deletions src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
local function esf(x: int):: int:
return 1

#local function esf(x: string):: int:
# return 2

#local function esf(x: ints):: int:
# return 3

local function esf_two(x: int, y: int):: int:
return 4

test "function section":
set {_x} to function esf with arguments:
x set to 1
assert {_x} = 1

#set {_x} to function esf with arguments:
# x set to "hey"
#assert {_x} = 2

#set {_x} to function esf with arguments:
# x set to 1 and 2
#assert {_x} = 3

parse:
function esf with arguments:
x set to firework
assert first element of last parse logs is set

#parse:
# function esf with arguments:
# x set to {_y}
#assert first element of last parse logs is set