Skip to content

Commit 78c9265

Browse files
committed
Improve tokenizer errors
1 parent 15ccc1d commit 78c9265

10 files changed

Lines changed: 144 additions & 58 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = "io.github.vaperion.blade"
9-
version = "1.1.4"
9+
version = "1.1.5"
1010

1111
subprojects {
1212
apply(plugin = "io.freefair.lombok")

bukkit/src/main/java/me/vaperion/blade/bukkit/container/BukkitContainer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public boolean execute(@NotNull CommandSender sender,
318318
context.sender().sendMessage(blade.configuration().messages().error(error.formatForChat()));
319319
command.usageMessage(context).sendTo(context);
320320

321-
if (!error.type().isSilent()) {
321+
if (error.type().shouldLog()) {
322322
blade.logger().error(
323323
"Failed to parse %s's command input for command `%s`: %s",
324324
sender.getName(),
@@ -475,15 +475,8 @@ public void tabComplete(@NotNull CommandSender sender,
475475
sender.getName(), commandLine);
476476
} catch (BladeFatalError ex) {
477477
wrap(sender).sendMessage(blade.configuration().messages().error(ex.getMessage()));
478-
} catch (TokenizerError error) {
479-
// Don't send tokenizer errors to the user during tab completion - just log them.
480-
481-
if (!error.type().isSilent()) {
482-
blade.logger().error(
483-
"Failed to parse %s's command input for command `%s`: %s",
484-
sender.getName(),
485-
commandLine, TokenizerError.generateFancyMessage(error));
486-
}
478+
} catch (TokenizerError ignored) {
479+
// Incomplete input is normal during tab completion.
487480
} catch (Throwable t) {
488481
blade.logger().error(t, "An error occurred while %s was tab completing the command `%s`.",
489482
sender.getName(), commandLine);

core/src/main/java/me/vaperion/blade/tokenizer/AbstractStringTokenizer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ public char take() throws TokenizerError {
6565
* Expects the current character to be whitespace.
6666
*/
6767
public void expectWhitespace() throws TokenizerError {
68-
expect(WHITESPACE_PRED);
68+
if (peekWhitespace()) {
69+
return;
70+
}
71+
72+
throw TokenizerError.expectedWhitespace(this, peek());
6973
}
7074

7175
/**
@@ -81,6 +85,26 @@ public void expect(char expected) throws TokenizerError {
8185
throw TokenizerError.unexpectedCharacter(this, expected, peek());
8286
}
8387

88+
/**
89+
* Expects the current character to match one of the given characters.
90+
*
91+
* @param expected the accepted characters
92+
*/
93+
public void expectOneOf(char @NotNull ... expected) throws TokenizerError {
94+
if (expected.length == 0) {
95+
throw new IllegalArgumentException("At least one expected character is required.");
96+
}
97+
98+
char actual = peek();
99+
for (char candidate : expected) {
100+
if (actual == candidate) {
101+
return;
102+
}
103+
}
104+
105+
throw TokenizerError.expectedOneOf(this, expected, actual);
106+
}
107+
84108
/**
85109
* Expects the current character to pass the given test.
86110
*

core/src/main/java/me/vaperion/blade/tokenizer/TokenizerError.java

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.jetbrains.annotations.NotNull;
55
import org.jetbrains.annotations.Nullable;
66

7+
import java.util.StringJoiner;
8+
79
@Getter
810
@SuppressWarnings("unused")
911
public class TokenizerError extends RuntimeException {
@@ -18,6 +20,34 @@ public static TokenizerError unexpectedEnd(@NotNull AbstractStringTokenizer toke
1820
);
1921
}
2022

23+
@NotNull
24+
public static TokenizerError expectedWhitespace(@NotNull AbstractStringTokenizer tokenizer,
25+
char actual) {
26+
return new TokenizerError(
27+
Type.EXPECTED_WHITESPACE,
28+
tokenizer,
29+
0,
30+
String.format("expected whitespace but found '%c'", actual)
31+
);
32+
}
33+
34+
@NotNull
35+
public static TokenizerError expectedOneOf(@NotNull AbstractStringTokenizer tokenizer,
36+
char @NotNull [] expected,
37+
char actual) {
38+
StringJoiner formattedExpected = new StringJoiner(", ", "[", "]");
39+
for (char character : expected) {
40+
formattedExpected.add(formatCharacter(character));
41+
}
42+
43+
return new TokenizerError(
44+
Type.EXPECTED_ONE_OF,
45+
tokenizer,
46+
0,
47+
String.format("expected one of %s but found %s", formattedExpected, formatCharacter(actual))
48+
);
49+
}
50+
2151
@NotNull
2252
public static TokenizerError unexpectedCharacter(@NotNull AbstractStringTokenizer tokenizer,
2353
@Nullable Character expected,
@@ -87,6 +117,23 @@ public synchronized Throwable fillInStackTrace() {
87117
return this;
88118
}
89119

120+
@NotNull
121+
private static String formatCharacter(char character) {
122+
switch (character) {
123+
case '\\':
124+
case '\'':
125+
return "'\\" + character + "'";
126+
case '\n':
127+
return "'\\n'";
128+
case '\r':
129+
return "'\\r'";
130+
case '\t':
131+
return "'\\t'";
132+
default:
133+
return "'" + character + "'";
134+
}
135+
}
136+
90137
@NotNull
91138
public String formatForChat() {
92139
return String.format(
@@ -101,17 +148,19 @@ public enum Type {
101148
UNEXPECTED_END,
102149
UNEXPECTED_CHARACTER,
103150
REQUIRED_N_CHARACTERS,
104-
MISSING_FLAG_VALUE(true),
151+
EXPECTED_WHITESPACE(false),
152+
EXPECTED_ONE_OF(false),
153+
MISSING_FLAG_VALUE(false),
105154
;
106155

107-
private final boolean isSilent;
156+
private final boolean shouldLog;
108157

109158
Type() {
110-
this(false);
159+
this(true);
111160
}
112161

113-
Type(boolean isSilent) {
114-
this.isSilent = isSilent;
162+
Type(boolean shouldLog) {
163+
this.shouldLog = shouldLog;
115164
}
116165
}
117166

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package me.vaperion.blade.tokenizer;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
6+
public class StringTokenizerTest {
7+
8+
@Test
9+
public void acceptsOneOfExpectedCharacters() {
10+
StringTokenizer tokenizer = new StringTokenizer(":");
11+
12+
tokenizer.expectOneOf(';', ':');
13+
14+
Assertions.assertEquals(':', tokenizer.take());
15+
}
16+
17+
@Test
18+
public void logsUnexpectedCharactersByDefault() {
19+
StringTokenizer tokenizer = new StringTokenizer(":");
20+
21+
TokenizerError error = Assertions.assertThrows(TokenizerError.class, () -> tokenizer.expect(';'));
22+
23+
Assertions.assertTrue(error.type().shouldLog());
24+
}
25+
26+
@Test
27+
public void reportsInvalidArgumentSeparatorAsUserInputError() {
28+
StringTokenizer tokenizer = new StringTokenizer(":");
29+
30+
TokenizerError error = Assertions.assertThrows(TokenizerError.class, tokenizer::expectWhitespace);
31+
32+
Assertions.assertEquals("expected whitespace but found ':'", error.getMessage());
33+
Assertions.assertFalse(error.type().shouldLog());
34+
}
35+
36+
@Test
37+
public void reportsExpectedCharactersAsUserInputError() {
38+
StringTokenizer tokenizer = new StringTokenizer(":");
39+
40+
TokenizerError error = Assertions.assertThrows(
41+
TokenizerError.class,
42+
() -> tokenizer.expectOneOf('"', '\'')
43+
);
44+
45+
Assertions.assertEquals("expected one of ['\"', '\\''] but found ':'", error.getMessage());
46+
Assertions.assertFalse(error.type().shouldLog());
47+
}
48+
}

fabric-legacy/src/main/java/me/vaperion/blade/fabric/container/FabricContainer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public boolean execute(@NotNull CommandContext<CommandSourceStack> ctx) {
183183
context.sender().sendMessage(blade.configuration().messages().error(error.formatForChat()));
184184
command.usageMessage(context).sendTo(context);
185185

186-
if (!error.type().isSilent()) {
186+
if (error.type().shouldLog()) {
187187
blade.logger().error(
188188
"Failed to parse %s's command input for command `%s`: %s",
189189
sender.getTextName(),
@@ -296,15 +296,8 @@ public void suggest(@NotNull CommandContext<CommandSourceStack> ctx,
296296
sender.getTextName(), label);
297297
} catch (BladeFatalError ex) {
298298
new FabricSender(blade, sender).sendMessage(blade.configuration().messages().error(ex.getMessage()));
299-
} catch (TokenizerError error) {
300-
// Don't send tokenizer errors to the user during tab completion - just log them.
301-
302-
if (!error.type().isSilent()) {
303-
blade.logger().error(
304-
"Failed to parse %s's command input for command `%s`: %s",
305-
sender.getTextName(),
306-
label, TokenizerError.generateFancyMessage(error));
307-
}
299+
} catch (TokenizerError ignored) {
300+
// Incomplete input is normal during tab completion.
308301
} catch (Throwable t) {
309302
blade.logger().error(t, "An error occurred while %s was tab completing the command `%s`.",
310303
sender.getTextName(), label);

fabric/src/main/java/me/vaperion/blade/fabric/container/FabricContainer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public boolean execute(@NotNull CommandContext<CommandSourceStack> ctx) {
183183
context.sender().sendMessage(blade.configuration().messages().error(error.formatForChat()));
184184
command.usageMessage(context).sendTo(context);
185185

186-
if (!error.type().isSilent()) {
186+
if (error.type().shouldLog()) {
187187
blade.logger().error(
188188
"Failed to parse %s's command input for command `%s`: %s",
189189
sender.getTextName(),
@@ -296,15 +296,8 @@ public void suggest(@NotNull CommandContext<CommandSourceStack> ctx,
296296
sender.getTextName(), label);
297297
} catch (BladeFatalError ex) {
298298
new FabricSender(blade, sender).sendMessage(blade.configuration().messages().error(ex.getMessage()));
299-
} catch (TokenizerError error) {
300-
// Don't send tokenizer errors to the user during tab completion - just log them.
301-
302-
if (!error.type().isSilent()) {
303-
blade.logger().error(
304-
"Failed to parse %s's command input for command `%s`: %s",
305-
sender.getTextName(),
306-
label, TokenizerError.generateFancyMessage(error));
307-
}
299+
} catch (TokenizerError ignored) {
300+
// Incomplete input is normal during tab completion.
308301
} catch (Throwable t) {
309302
blade.logger().error(t, "An error occurred while %s was tab completing the command `%s`.",
310303
sender.getTextName(), label);

hytale/src/main/java/me/vaperion/blade/hytale/container/HytaleContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public void unregister() {
188188
} catch (TokenizerError error) {
189189
context.sender().sendMessage(blade.configuration().messages().error(error.formatForChat()));
190190

191-
if (!error.type().isSilent()) {
191+
if (error.type().shouldLog()) {
192192
blade.logger().error(
193193
"Failed to parse %s's command input for command `%s`: %s",
194194
sender.toString(),

minestom/src/main/java/me/vaperion/blade/minestom/container/MinestomContainer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ private void execute(@NotNull CommandSender sender,
215215
messages.error(error.formatForChat())
216216
);
217217

218-
if (!error.type().isSilent()) {
218+
if (error.type().shouldLog()) {
219219
blade.logger().error(
220220
"Failed to parse %s's command input for command `%s`: %s",
221221
sender.toString(),
@@ -351,15 +351,8 @@ private List<String> doSuggest(@NotNull CommandSender sender,
351351
sender.sendMessage(
352352
blade.configuration().messages().error(ex.getMessage())
353353
);
354-
} catch (TokenizerError error) {
355-
// Don't send tokenizer errors to the user during tab completion - just log them.
356-
357-
if (!error.type().isSilent()) {
358-
blade.logger().error(
359-
"Failed to parse %s's command input for command `%s`: %s",
360-
sender.toString(),
361-
label, TokenizerError.generateFancyMessage(error));
362-
}
354+
} catch (TokenizerError ignored) {
355+
// Incomplete input is normal during tab completion.
363356
} catch (Throwable t) {
364357
blade.logger().error(t, "An error occurred while %s was tab completing the command `%s`.",
365358
sender.toString(), label);

velocity/src/main/java/me/vaperion/blade/velocity/container/VelocityContainer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public void execute(Invocation invocation) {
203203
messages.error(error.formatForChat())
204204
);
205205

206-
if (!error.type().isSilent()) {
206+
if (error.type().shouldLog()) {
207207
blade.logger().error(
208208
"Failed to parse %s's command input for command `%s`: %s",
209209
sender.toString(),
@@ -306,15 +306,8 @@ public List<String> suggest(Invocation invocation) {
306306
sender.sendMessage(
307307
blade.configuration().messages().error(ex.getMessage())
308308
);
309-
} catch (TokenizerError error) {
310-
// Don't send tokenizer errors to the user during tab completion - just log them.
311-
312-
if (!error.type().isSilent()) {
313-
blade.logger().error(
314-
"Failed to parse %s's command input for command `%s`: %s",
315-
sender.toString(),
316-
label, TokenizerError.generateFancyMessage(error));
317-
}
309+
} catch (TokenizerError ignored) {
310+
// Incomplete input is normal during tab completion.
318311
} catch (Throwable t) {
319312
blade.logger().error(t, "An error occurred while %s was tab completing the command `%s`.",
320313
sender.toString(), label);

0 commit comments

Comments
 (0)