Added functionality for finding ambiguities on-the-fly

This commit is contained in:
Nathan Adams 2018-01-30 16:16:36 +01:00
parent 3498398452
commit 69c2a248de
18 changed files with 194 additions and 40 deletions

View file

@ -3,7 +3,7 @@ import groovy.io.FileType
apply plugin: 'java-library'
apply plugin: 'maven'
version = '0.1.19'
version = '0.1.21'
group = 'com.mojang'
task wrapper(type: Wrapper) {

View file

@ -0,0 +1,10 @@
package com.mojang.brigadier;
import com.mojang.brigadier.tree.CommandNode;
import java.util.Collection;
@FunctionalInterface
public interface AmbiguityConsumer<S> {
void ambiguous(final CommandNode<S> parent, final CommandNode<S> child, final CommandNode<S> sibling, final Collection<String> inputs);
}

View file

@ -427,6 +427,10 @@ public class CommandDispatcher<S> {
return node;
}
public void findAmbiguities(final AmbiguityConsumer<S> consumer) {
root.findAmbiguities(consumer);
}
private void addPaths(final CommandNode<S> node, final List<List<CommandNode<S>>> result, final List<CommandNode<S>> parents) {
final List<CommandNode<S>> current = new ArrayList<>(parents);
current.add(node);

View file

@ -7,12 +7,18 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
public interface ArgumentType<T> {
<S> T parse(StringReader reader, CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException;
<S> T parse(StringReader reader) throws CommandSyntaxException;
default <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) {
return Suggestions.empty();
}
default Collection<String> getExamples() {
return Collections.emptyList();
}
}

View file

@ -7,9 +7,13 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
public class BoolArgumentType implements ArgumentType<Boolean> {
private static final Collection<String> EXAMPLES = Arrays.asList("true", "false");
private BoolArgumentType() {
}
@ -22,7 +26,7 @@ public class BoolArgumentType implements ArgumentType<Boolean> {
}
@Override
public <S> Boolean parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
public <S> Boolean parse(final StringReader reader) throws CommandSyntaxException {
return reader.readBoolean();
}
@ -36,4 +40,9 @@ public class BoolArgumentType implements ArgumentType<Boolean> {
}
return builder.buildFuture();
}
@Override
public Collection<String> getExamples() {
return EXAMPLES;
}
}

View file

@ -6,9 +6,13 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Arrays;
import java.util.Collection;
public class DoubleArgumentType implements ArgumentType<Double> {
public static final ParameterizedCommandExceptionType ERROR_TOO_SMALL = new ParameterizedCommandExceptionType("argument.double.low", "Double must not be less than ${minimum}, found ${found}", "found", "minimum");
public static final ParameterizedCommandExceptionType ERROR_TOO_BIG = new ParameterizedCommandExceptionType("argument.double.big", "Double must not be more than ${maximum}, found ${found}", "found", "maximum");
private static final Collection<String> EXAMPLES = Arrays.asList("0", "1.2", ".5", "-1", "-.5", "-1234.56");
private final double minimum;
private final double maximum;
@ -43,7 +47,7 @@ public class DoubleArgumentType implements ArgumentType<Double> {
}
@Override
public <S> Double parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
public <S> Double parse(final StringReader reader) throws CommandSyntaxException {
final int start = reader.getCursor();
final double result = reader.readDouble();
if (result < minimum) {
@ -81,4 +85,9 @@ public class DoubleArgumentType implements ArgumentType<Double> {
return "double(" + minimum + ", " + maximum + ")";
}
}
@Override
public Collection<String> getExamples() {
return EXAMPLES;
}
}

View file

@ -6,9 +6,13 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Arrays;
import java.util.Collection;
public class FloatArgumentType implements ArgumentType<Float> {
public static final ParameterizedCommandExceptionType ERROR_TOO_SMALL = new ParameterizedCommandExceptionType("argument.float.low", "Float must not be less than ${minimum}, found ${found}", "found", "minimum");
public static final ParameterizedCommandExceptionType ERROR_TOO_BIG = new ParameterizedCommandExceptionType("argument.float.big", "Float must not be more than ${maximum}, found ${found}", "found", "maximum");
private static final Collection<String> EXAMPLES = Arrays.asList("0", "1.2", ".5", "-1", "-.5", "-1234.56");
private final float minimum;
private final float maximum;
@ -43,7 +47,7 @@ public class FloatArgumentType implements ArgumentType<Float> {
}
@Override
public <S> Float parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
public <S> Float parse(final StringReader reader) throws CommandSyntaxException {
final int start = reader.getCursor();
final float result = reader.readFloat();
if (result < minimum) {
@ -81,4 +85,9 @@ public class FloatArgumentType implements ArgumentType<Float> {
return "float(" + minimum + ", " + maximum + ")";
}
}
@Override
public Collection<String> getExamples() {
return EXAMPLES;
}
}

View file

@ -6,9 +6,13 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Arrays;
import java.util.Collection;
public class IntegerArgumentType implements ArgumentType<Integer> {
public static final ParameterizedCommandExceptionType ERROR_TOO_SMALL = new ParameterizedCommandExceptionType("argument.integer.low", "Integer must not be less than ${minimum}, found ${found}", "found", "minimum");
public static final ParameterizedCommandExceptionType ERROR_TOO_BIG = new ParameterizedCommandExceptionType("argument.integer.big", "Integer must not be more than ${maximum}, found ${found}", "found", "maximum");
private static final Collection<String> EXAMPLES = Arrays.asList("0", "123", "-123");
private final int minimum;
private final int maximum;
@ -43,7 +47,7 @@ public class IntegerArgumentType implements ArgumentType<Integer> {
}
@Override
public <S> Integer parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
public <S> Integer parse(final StringReader reader) throws CommandSyntaxException {
final int start = reader.getCursor();
final int result = reader.readInt();
if (result < minimum) {

View file

@ -1,11 +1,13 @@
package com.mojang.brigadier.arguments;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import java.util.Arrays;
import java.util.Collection;
public class StringArgumentType implements ArgumentType<String> {
private final StringType type;
@ -34,7 +36,7 @@ public class StringArgumentType implements ArgumentType<String> {
}
@Override
public <S> String parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
public <S> String parse(final StringReader reader) throws CommandSyntaxException {
if (type == StringType.GREEDY_PHRASE) {
final String text = reader.getRemaining();
reader.setCursor(reader.getTotalLength());
@ -51,6 +53,11 @@ public class StringArgumentType implements ArgumentType<String> {
return "string()";
}
@Override
public Collection<String> getExamples() {
return type.getExamples();
}
public static String escapeIfRequired(final String input) {
for (final char c : input.toCharArray()) {
if (!StringReader.isAllowedInUnquotedString(c)) {
@ -76,8 +83,18 @@ public class StringArgumentType implements ArgumentType<String> {
}
public enum StringType {
SINGLE_WORD,
QUOTABLE_PHRASE,
GREEDY_PHRASE,
SINGLE_WORD("word", "words_with_underscores"),
QUOTABLE_PHRASE("\"quoted phrase\"", "word", "\"\""),
GREEDY_PHRASE("word", "words with spaces", "\"and symbols\""),;
private final Collection<String> examples;
StringType(final String... examples) {
this.examples = Arrays.asList(examples);
}
public Collection<String> getExamples() {
return examples;
}
}
}

View file

@ -13,6 +13,7 @@ import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
@ -52,7 +53,7 @@ public class ArgumentCommandNode<S, T> extends CommandNode<S> {
@Override
public void parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
final int start = reader.getCursor();
final T result = type.parse(reader, contextBuilder);
final T result = type.parse(reader);
final ParsedArgument<S, T> parsed = new ParsedArgument<>(start, reader.getCursor(), result);
contextBuilder.withArgument(name, parsed);
@ -80,6 +81,17 @@ public class ArgumentCommandNode<S, T> extends CommandNode<S> {
return builder;
}
@Override
public boolean isValidInput(final String input) {
try {
final StringReader reader = new StringReader(input);
type.parse(reader);
return !reader.canRead() || reader.peek() == ' ';
} catch (final CommandSyntaxException ignored) {
return false;
}
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@ -103,4 +115,9 @@ public class ArgumentCommandNode<S, T> extends CommandNode<S> {
protected String getSortedKey() {
return name;
}
@Override
public Collection<String> getExamples() {
return type.getExamples();
}
}

View file

@ -2,6 +2,8 @@ package com.mojang.brigadier.tree;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.mojang.brigadier.AmbiguityConsumer;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.RedirectModifier;
import com.mojang.brigadier.StringReader;
@ -16,6 +18,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -77,6 +80,18 @@ public abstract class CommandNode<S> implements Comparable<CommandNode<S>> {
child.addChild(grandchild);
}
} else {
for (final CommandNode<S> sibling : children.values()) {
for (final String example : node.getExamples()) {
if (sibling.isValidInput(example)) {
System.out.println("Ambiguity detected in " + getName() + ", siblings " + sibling.getName() + " and " + node.getName() + " can both parse '" + example + "' successfully");
}
}
for (final String example : sibling.getExamples()) {
if (node.isValidInput(example)) {
System.out.println("Ambiguity detected in " + getName() + ", siblings " + sibling.getName() + " and " + node.getName() + " can both parse '" + example + "' successfully");
}
}
}
children.put(node.getName(), node);
if (node instanceof LiteralCommandNode) {
literals.put(node.getName(), (LiteralCommandNode<S>) node);
@ -88,6 +103,33 @@ public abstract class CommandNode<S> implements Comparable<CommandNode<S>> {
children = children.entrySet().stream().sorted(Map.Entry.comparingByValue()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
public void findAmbiguities(final AmbiguityConsumer<S> consumer) {
Set<String> matches = Sets.newHashSet();
for (final CommandNode<S> child : children.values()) {
for (final CommandNode<S> sibling : children.values()) {
if (child == sibling) {
continue;
}
for (final String input : child.getExamples()) {
if (sibling.isValidInput(input)) {
matches.add(input);
}
}
if (matches.size() > 0) {
consumer.ambiguous(this, child, sibling, matches);
matches = Sets.newHashSet();
}
}
child.findAmbiguities(consumer);
}
}
protected abstract boolean isValidInput(final String input);
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@ -153,4 +195,6 @@ public abstract class CommandNode<S> implements Comparable<CommandNode<S>> {
public boolean isFork() {
return forks;
}
public abstract Collection<String> getExamples();
}

View file

@ -12,6 +12,8 @@ import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
@ -36,18 +38,30 @@ public class LiteralCommandNode<S> extends CommandNode<S> {
@Override
public void parse(final StringReader reader, final CommandContextBuilder<S> contextBuilder) throws CommandSyntaxException {
final int start = reader.getCursor();
final int end = parse(reader);
if (end > -1) {
contextBuilder.withNode(this, StringRange.between(start, end));
return;
}
throw ERROR_INCORRECT_LITERAL.createWithContext(reader, literal);
}
private int parse(final StringReader reader) {
final int start = reader.getCursor();
if (reader.canRead(literal.length())) {
final int end = start + literal.length();
if (reader.getString().substring(start, end).equals(literal)) {
reader.setCursor(end);
contextBuilder.withNode(this, StringRange.between(start, end));
return;
if (!reader.canRead() || reader.peek() == ' ') {
return end;
} else {
reader.setCursor(start);
}
}
}
reader.setCursor(start);
throw ERROR_INCORRECT_LITERAL.createWithContext(reader, literal);
return -1;
}
@Override
@ -59,6 +73,11 @@ public class LiteralCommandNode<S> extends CommandNode<S> {
}
}
@Override
public boolean isValidInput(final String input) {
return parse(new StringReader(input)) > -1;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@ -97,4 +116,9 @@ public class LiteralCommandNode<S> extends CommandNode<S> {
protected String getSortedKey() {
return literal;
}
@Override
public Collection<String> getExamples() {
return Collections.singleton(literal);
}
}

View file

@ -8,6 +8,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
@ -35,6 +36,11 @@ public class RootCommandNode<S> extends CommandNode<S> {
return Suggestions.empty();
}
@Override
public boolean isValidInput(final String input) {
return false;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@ -51,4 +57,9 @@ public class RootCommandNode<S> extends CommandNode<S> {
protected String getSortedKey() {
return "";
}
@Override
public Collection<String> getExamples() {
return Collections.emptyList();
}
}

View file

@ -9,7 +9,6 @@ import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
@ -31,7 +30,7 @@ public class BoolArgumentTypeTest {
public void parse() throws Exception {
final StringReader reader = mock(StringReader.class);
when(reader.readBoolean()).thenReturn(true);
assertThat(type.parse(reader, context), is(true));
assertThat(type.parse(reader), is(true));
verify(reader).readBoolean();
}
}

View file

@ -32,7 +32,7 @@ public class DoubleArgumentTypeTest {
@Test
public void parse() throws Exception {
final StringReader reader = new StringReader("15");
assertThat(doubleArg().parse(reader, context), is(15.0));
assertThat(doubleArg().parse(reader), is(15.0));
assertThat(reader.canRead(), is(false));
}
@ -40,7 +40,7 @@ public class DoubleArgumentTypeTest {
public void parse_tooSmall() throws Exception {
final StringReader reader = new StringReader("-5");
try {
doubleArg(0, 100).parse(reader, context);
doubleArg(0, 100).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(DoubleArgumentType.ERROR_TOO_SMALL));
@ -53,7 +53,7 @@ public class DoubleArgumentTypeTest {
public void parse_tooBig() throws Exception {
final StringReader reader = new StringReader("5");
try {
doubleArg(-100, 0).parse(reader, context);
doubleArg(-100, 0).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(DoubleArgumentType.ERROR_TOO_BIG));

View file

@ -1,7 +1,6 @@
package com.mojang.brigadier.arguments;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContextBuilder;
@ -10,13 +9,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Set;
import static com.mojang.brigadier.arguments.FloatArgumentType.floatArg;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
@ -37,7 +32,7 @@ public class FloatArgumentTypeTest {
@Test
public void parse() throws Exception {
final StringReader reader = new StringReader("15");
assertThat(floatArg().parse(reader, context), is(15f));
assertThat(floatArg().parse(reader), is(15f));
assertThat(reader.canRead(), is(false));
}
@ -45,7 +40,7 @@ public class FloatArgumentTypeTest {
public void parse_tooSmall() throws Exception {
final StringReader reader = new StringReader("-5");
try {
floatArg(0, 100).parse(reader, context);
floatArg(0, 100).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(FloatArgumentType.ERROR_TOO_SMALL));
@ -58,7 +53,7 @@ public class FloatArgumentTypeTest {
public void parse_tooBig() throws Exception {
final StringReader reader = new StringReader("5");
try {
floatArg(-100, 0).parse(reader, context);
floatArg(-100, 0).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(FloatArgumentType.ERROR_TOO_BIG));

View file

@ -32,7 +32,7 @@ public class IntegerArgumentTypeTest {
@Test
public void parse() throws Exception {
final StringReader reader = new StringReader("15");
assertThat(integer().parse(reader, context), is(15));
assertThat(integer().parse(reader), is(15));
assertThat(reader.canRead(), is(false));
}
@ -40,7 +40,7 @@ public class IntegerArgumentTypeTest {
public void parse_tooSmall() throws Exception {
final StringReader reader = new StringReader("-5");
try {
integer(0, 100).parse(reader, context);
integer(0, 100).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(IntegerArgumentType.ERROR_TOO_SMALL));
@ -53,7 +53,7 @@ public class IntegerArgumentTypeTest {
public void parse_tooBig() throws Exception {
final StringReader reader = new StringReader("5");
try {
integer(-100, 0).parse(reader, context);
integer(-100, 0).parse(reader);
fail();
} catch (final CommandSyntaxException ex) {
assertThat(ex.getType(), is(IntegerArgumentType.ERROR_TOO_BIG));

View file

@ -1,6 +1,5 @@
package com.mojang.brigadier.arguments;
import com.google.common.collect.Sets;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContextBuilder;
import org.junit.Test;
@ -8,13 +7,10 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Set;
import static com.mojang.brigadier.arguments.StringArgumentType.escapeIfRequired;
import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
import static com.mojang.brigadier.arguments.StringArgumentType.string;
import static com.mojang.brigadier.arguments.StringArgumentType.word;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
@ -32,7 +28,7 @@ public class StringArgumentTypeTest {
public void testParseWord() throws Exception {
final StringReader reader = mock(StringReader.class);
when(reader.readUnquotedString()).thenReturn("hello");
assertThat(word().parse(reader, context), equalTo("hello"));
assertThat(word().parse(reader), equalTo("hello"));
verify(reader).readUnquotedString();
}
@ -40,14 +36,14 @@ public class StringArgumentTypeTest {
public void testParseString() throws Exception {
final StringReader reader = mock(StringReader.class);
when(reader.readString()).thenReturn("hello world");
assertThat(string().parse(reader, context), equalTo("hello world"));
assertThat(string().parse(reader), equalTo("hello world"));
verify(reader).readString();
}
@Test
public void testParseGreedyString() throws Exception {
final StringReader reader = new StringReader("Hello world! This is a test.");
assertThat(greedyString().parse(reader, context), equalTo("Hello world! This is a test."));
assertThat(greedyString().parse(reader), equalTo("Hello world! This is a test."));
assertThat(reader.canRead(), is(false));
}