Skip to content

Commit

Permalink
Rework Parser definition
Browse files Browse the repository at this point in the history
Instead of setting up logic via builders, parsers are usually just lambdas. The result of parsers can be adapted using `Result#continueWith`.

Also fixed inconsistency with vanilla behavior in `IntegerRange` parsing brought up in #575 (comment).
  • Loading branch information
willkroboth committed Oct 25, 2024
1 parent 5d58bda commit d7c798e
Show file tree
Hide file tree
Showing 13 changed files with 530 additions and 954 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.BuiltInExceptionProvider;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import dev.jorel.commandapi.BukkitTooltip;
import dev.jorel.commandapi.arguments.parser.Parser;
import dev.jorel.commandapi.arguments.parser.ParserArgument;
import dev.jorel.commandapi.arguments.parser.ParserLiteral;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;

import java.util.function.Function;

/**
* Utilities for creating mock argument parsers
*/
Expand Down Expand Up @@ -53,42 +51,17 @@ public static Message translatedMessage(String key, Object... args) {
}

// Parser utilities
/**
* A placeholder {@link CommandSyntaxException} that a parser can throw if it doesn't match the input.
* Typically, this exception will be caught immediately using {@link Parser.ExceptionHandler#neverThrowException()},
* and parsing will continue to the next branch in a {@link Parser#tryParse(Parser.NonTerminal)} chain.
*/
public static final CommandSyntaxException NEXT_BRANCH = new SimpleCommandExceptionType(
() -> "This branch did not match"
).create();

/**
* Returns a new {@link Parser.Literal}. When the returned parser is invoked, if {@link StringReader#canRead()}
* returns {@code false}, then a {@link CommandSyntaxException} will be thrown according to the given {@code exception}
* {@link Function}. If {@link StringReader#canRead()} returns {@code true}, then the returned parser succeeds.
*
* @param exception A {@link Function} that creates a {@link CommandSyntaxException} when the input
* {@link StringReader} does not have any more characters to read.
* @return A {@link Parser.Literal} that checks if the input {@link StringReader} has characters to read.
*/
public static Parser.Literal assertCanRead(Function<StringReader, CommandSyntaxException> exception) {
return reader -> {
if (!reader.canRead()) {
throw exception.apply(reader);
}
};
}

/**
* Returns a new {@link Parser.Literal}. When the returned parser is invoked, it tries to read the given
* Returns a new {@link ParserLiteral}. When the returned parser is invoked, it tries to read the given
* {@code literal} String from the input {@link StringReader}. If the {@code literal} is present, this parser
* succeeds and moves {@link StringReader#getCursor()} to the end of the {@code literal}. Otherwise, this parser
* will fail and throw a {@link CommandSyntaxException} with type {@link BuiltInExceptionProvider#literalIncorrect()}.
*
* @param literal The exact String that is expected to be at the start of the input {@link StringReader}.
* @return A {@link Parser.Literal} that checks if the {@code literal} String can be read from the input {@link StringReader}.
* @return A {@link ParserLiteral} that checks if the {@code literal} String can be read from the input {@link StringReader}.
*/
public static Parser.Literal literal(String literal) {
public static ParserLiteral literal(String literal) {
return reader -> {
if (reader.canRead(literal.length())) {
int start = reader.getCursor();
Expand All @@ -104,7 +77,7 @@ public static Parser.Literal literal(String literal) {
}

/**
* Returns a new {@link Parser.Argument} that reads characters from the input {@link StringReader} until it reaches
* Returns a new {@link ParserArgument} that reads characters from the input {@link StringReader} until it reaches
* the given terminator character. If the terminator character is not found, the entire
* {@link StringReader#getRemaining()} String will be read.
* <p>
Expand All @@ -116,11 +89,11 @@ public static Parser.Literal literal(String literal) {
* if this happens.
*
* @param terminator The character to stop reading at.
* @return A {@link Parser.Argument} that reads until it finds the given terminator. Note that the returned String will
* @return A {@link ParserArgument} that reads until it finds the given terminator. Note that the returned String will
* include the terminator at the end, unless the end of the input {@link StringReader} is reached without finding the
* terminator.
*/
public static Parser.Argument<String> readUntilWithoutEscapeCharacter(char terminator) {
public static ParserArgument<String> readUntilWithoutEscapeCharacter(char terminator) {
return reader -> {
int start = reader.getCursor();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static EntitySelectorArgumentType player() {

@Override
public EntitySelector parse(StringReader reader) throws CommandSyntaxException {
EntitySelector entityselector = EntitySelectorParser.PARSER.parse(reader);
EntitySelector entityselector = EntitySelectorParser.parser.parse(reader);
// I don't know why Minecraft does `reader.setCursor(0)` here before throwing exceptions, but it does ¯\_(ツ)_/¯
// That has the goofy result of underlining the whole command when it should really only underline the selector
// This is easily fixed, just store `reader.getCursor()` before parsing the selector
Expand All @@ -73,7 +73,7 @@ public EntitySelector parse(StringReader reader) throws CommandSyntaxException {

@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
return EntitySelectorParser.PARSER.listSuggestions(context, builder);
return EntitySelectorParser.parser.listSuggestions(context, builder);
}

public static List<? extends Entity> findManyEntities(CommandContext<MockCommandSource> cmdCtx, String key, boolean allowEmpty) throws CommandSyntaxException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import dev.jorel.commandapi.UnimplementedMethodException;
import dev.jorel.commandapi.arguments.parser.ParameterGetter;
import dev.jorel.commandapi.arguments.parser.Parser;
import dev.jorel.commandapi.arguments.parser.ParserLiteral;
import dev.jorel.commandapi.arguments.parser.Result;
import dev.jorel.commandapi.arguments.parser.SuggestionProvider;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;

import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;

public class EntitySelectorParser {
Expand Down Expand Up @@ -48,16 +50,12 @@ private EntitySelector build() {
}

// Parsing
private static final Parser.Literal isSelectorStart = reader -> {
if (!(reader.canRead() && reader.peek() == '@')) throw ArgumentUtilities.NEXT_BRANCH;
};

private static Parser.Literal parseSelector(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
return reader -> {
private static ParserLiteral parseSelector(EntitySelectorParser selectorBuilder) {
return Parser.read(reader -> {
reader.skip(); // skip @
if (!reader.canRead()) throw ERROR_MISSING_SELECTOR_TYPE.createWithContext(reader);
char selectorCode = reader.read();
EntitySelectorParser selectorBuilder = selectorBuilderGetter.get();

switch (selectorCode) {
case 'p' -> {
selectorBuilder.maxResults = 1;
Expand Down Expand Up @@ -101,29 +99,19 @@ private static Parser.Literal parseSelector(ParameterGetter<EntitySelectorParser
throw ERROR_UNKNOWN_SELECTOR_TYPE.createWithContext(reader, "@" + selectorCode);
}
}
};
}).suggests(suggestSelector);
}

private static final Parser.Literal isSelectorOptionsStart = reader -> {
if (!(reader.canRead() && reader.peek() == '[')) throw ArgumentUtilities.NEXT_BRANCH;
};

private static Parser.Literal parseSelectorOptions(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
private static ParserLiteral parseSelectorOptions(EntitySelectorParser selectorBuilder) {
return reader -> {
// TODO: Implement looping to parse these selector options
// I'm pretty sure it would basically reuse many other object parsers as well, so maybe do those first
throw new UnimplementedMethodException("Entity selectors with options are not supported");
};
}

private static final Parser.Literal isNameStart = reader -> {
if (!(reader.canRead() && reader.peek() != ' ')) throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader);
};

private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
return reader -> {
EntitySelectorParser selectorBuilder = selectorBuilderGetter.get();

private static ParserLiteral parseNameOrUUID(EntitySelectorParser selectorBuilder) {
return Parser.read(reader -> {
int start = reader.getCursor();
String input = reader.readString();
try {
Expand All @@ -132,7 +120,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorPars
selectorBuilder.includesEntities = true;
} catch (IllegalArgumentException ignored) {
// Not a valid UUID string
if (input.length() > 16) {
if (input.isEmpty() || input.length() > 16) {
// Also not a valid player name
reader.setCursor(start);
throw ERROR_INVALID_NAME_OR_UUID.createWithContext(reader);
Expand All @@ -143,11 +131,7 @@ private static Parser.Literal parseNameOrUUID(ParameterGetter<EntitySelectorPars
}

selectorBuilder.maxResults = 1;
};
}

private static Parser.Argument<EntitySelector> conclude(ParameterGetter<EntitySelectorParser> selectorBuilderGetter) {
return reader -> selectorBuilderGetter.get().build();
}).suggests(suggestName);
}

private static final SuggestionProvider suggestName = (context, builder) -> {
Expand All @@ -173,44 +157,43 @@ private static Parser.Argument<EntitySelector> conclude(ParameterGetter<EntitySe
suggestName.addSuggestions(context, builder);
};
private static final SuggestionProvider suggestOpenOptions = (context, builder) -> builder.suggest("[");
private static final SuggestionProvider suggestOptionsKeyOrClose = (context, builder) -> {
throw new UnimplementedMethodException("Entity selectors with options are not supported");
};

public static final Parser<EntitySelector> PARSER = Parser
.parse(reader -> new EntitySelectorParser())
.suggests(suggestNameOrSelector)
.alwaysThrowException()
.continueWith(selectorBuilder ->
Parser.tryParse(Parser.read(isSelectorStart)
.neverThrowException()
.continueWith(
Parser.read(parseSelector(selectorBuilder))
.suggests(suggestSelector)
.alwaysThrowException()
.continueWith(
Parser.tryParse(Parser.read(isSelectorOptionsStart)
.suggests(suggestOpenOptions)
.neverThrowException()
.continueWith(
Parser.read(parseSelectorOptions(selectorBuilder))
.suggests(suggestOptionsKeyOrClose)
.alwaysThrowException()
// Input @?[???]
.continueWith(conclude(selectorBuilder))
)
).then(conclude(selectorBuilder)) // Input @?
)
)
).then(Parser.read(isNameStart)
.alwaysThrowException()
.continueWith(
Parser.read(parseNameOrUUID(selectorBuilder))
.suggests(suggestName)
.alwaysThrowException()
// Input name or uuid
.continueWith(conclude(selectorBuilder))
)
)
public static final Parser<EntitySelector> parser = reader -> {
if (!reader.canRead()) {
// Empty input
return Result.withExceptionAndSuggestions(ERROR_INVALID_NAME_OR_UUID.createWithContext(reader), reader.getCursor(), suggestNameOrSelector);
}

// Build our selector
EntitySelectorParser selectorBuilder = new EntitySelectorParser();
Function<Result.Void, Result<EntitySelector>> conclude = Result.wrapFunctionResult(success -> selectorBuilder.build());

if (reader.peek() == '@') {
// Looks like selector
return parseSelector(selectorBuilder).getResult(reader).continueWith(
// Successfully read selector
success -> {
if (reader.canRead() && reader.peek() == '[') {
// Looks like includes selector options
return parseSelectorOptions(selectorBuilder).getResult(reader).continueWith(
// If successful, build the final selector
conclude
// Otherwise, pass original exception
);
}

// Otherwise, valid selector, but suggest opening options
return Result.withValueAndSuggestions(selectorBuilder.build(), reader.getCursor(), suggestOpenOptions);
}
// Otherwise pass original exception
);
}

// Looks like name/uuid
return parseNameOrUUID(selectorBuilder).getResult(reader).continueWith(
// If successful, build the final selector
conclude
// Otherwise pass original exception
);
};
}
Loading

0 comments on commit d7c798e

Please sign in to comment.