Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
125d115
init commit
Efnilite Aug 6, 2025
18ba815
use FunctionRegistry
Efnilite Aug 6, 2025
e1dcf5d
init commit
Efnilite Aug 6, 2025
d82ac87
requested changes
Efnilite Aug 7, 2025
29dd1a1
fix variables
Efnilite Aug 7, 2025
17e0431
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Sep 28, 2025
f326062
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Oct 26, 2025
c738ad5
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Dec 24, 2025
15e6311
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Jan 5, 2026
b9bb5f5
Optimize imports
Efnilite Jan 5, 2026
1ec8b04
Update to use overloading
Efnilite Jan 5, 2026
51f53eb
Move to common module, use FunctionReferenceParser
Efnilite Jan 5, 2026
4360e91
Update test function name
Efnilite Jan 5, 2026
62a5448
Update test error
Efnilite Jan 5, 2026
bb8a5a4
Translations and standardize errors
Efnilite Jan 5, 2026
a64d625
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Jan 10, 2026
0d44bfb
Minor changes
Efnilite Jan 28, 2026
20c76aa
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Jan 28, 2026
4fdc5de
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Apr 8, 2026
a3d964b
Merge ExprResult
Efnilite Apr 19, 2026
749ea65
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Apr 19, 2026
e71a0e8
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Aug 11, 2026
7bacf96
merge with dev/feature
Efnilite Aug 11, 2026
631a396
Merge branch 'dev/feature' into feature/multiline-function-args
Efnilite Aug 14, 2026
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> {
Comment thread
Efnilite marked this conversation as resolved.
Outdated

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()));
Comment thread
Efnilite marked this conversation as resolved.
Outdated

static {
Skript.registerExpression(ExprSecFunction.class, Object.class, ExpressionType.SIMPLE, "function <.+> with argument[s]");
Comment thread
Efnilite marked this conversation as resolved.
Outdated
Comment thread
Efnilite marked this conversation as resolved.
Outdated
}

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) {
Comment thread
Efnilite marked this conversation as resolved.
Outdated
if (node == null) {
Comment thread
Efnilite marked this conversation as resolved.
Outdated
Skript.error("A section must follow this expression.");
return false;
} else if (node.isEmpty()) {
Skript.error("A function section must contain code.");
Comment thread
Efnilite marked this conversation as resolved.
Outdated
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());
Comment thread
Efnilite marked this conversation as resolved.
Outdated

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