diff --git a/java/.mvn/jvm.config b/java/.mvn/jvm.config new file mode 100644 index 000000000..32599cefe --- /dev/null +++ b/java/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/java/checkstyle-suppressions.xml b/java/checkstyle-suppressions.xml new file mode 100644 index 000000000..aae028f2a --- /dev/null +++ b/java/checkstyle-suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/java/gherkin-java.razor b/java/gherkin-java.razor index 675175ba7..7602a3644 100644 --- a/java/gherkin-java.razor +++ b/java/gherkin-java.razor @@ -16,7 +16,7 @@ @helper HandleParserError(IEnumerable expectedTokens, State state) { final String stateComment = "State: @state.Id - @Raw(state.Comment)"; - List expectedTokens = asList("@Raw(string.Join("\", \"", expectedTokens))"); + List expectedTokens = List.of("@Raw(string.Join("\", \"", expectedTokens))"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -30,57 +30,21 @@ package io.cucumber.gherkin; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Queue; import java.util.function.BiConsumer; import java.util.function.BiPredicate; -import static java.util.Arrays.asList; - /** * This class was generated by Berp. *

* Changes to this class will be lost if the code is regenerated. */ final class @Model.ParserClassName { - enum TokenType { - None(RuleType.None), - @foreach(var rule in Model.RuleSet.TokenRules) - { @(rule.Name.Replace("#", ""))(RuleType.@(rule.Name.Replace("#", "_"))), -} ; - - final RuleType ruleType; - - TokenType(RuleType ruleType) { - this.ruleType = ruleType; - } - } - - enum RuleType { - None, - @foreach(var rule in Model.RuleSet.Where(r => !r.TempRule)) - { @rule.Name.Replace("#", "_"), // @rule.ToString(true) -} ; - - } private final Builder builder; - static final class ParserContext { - final TokenScanner tokenScanner; - final TokenMatcher tokenMatcher; - final Queue tokenQueue; - final List errors; - - ParserContext(TokenScanner tokenScanner, TokenMatcher tokenMatcher, Queue tokenQueue, List errors) { - this.tokenScanner = tokenScanner; - this.tokenMatcher = tokenMatcher; - this.tokenQueue = tokenQueue; - this.errors = errors; - } - } - Parser(Builder builder) { this.builder = builder; } @@ -104,7 +68,7 @@ final class @Model.ParserClassName { ParserContext context = new ParserContext( tokenScanner, tokenMatcher, - new LinkedList<>(), + new ArrayDeque<>(), new ArrayList<>() ); @@ -128,7 +92,7 @@ final class @Model.ParserClassName { private void addError(ParserContext context, ParserException error) { String newErrorMessage = error.getMessage(); for (ParserException e : context.errors) { - if (e.getMessage().equals(newErrorMessage)) { + if (Objects.equals(e.getMessage(), newErrorMessage)) { return; } } @@ -191,17 +155,15 @@ final class @Model.ParserClassName { } private int matchToken(int state, Token token, ParserContext context) { - int newState; - switch (state) { + int newState = switch (state) { @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) { - @:case @state.Id: - @:newState = matchTokenAt_@(state.Id)(token, context); - @:break; + @:case @state.Id -> matchTokenAt_@(state.Id)(token, context); } - default: + default -> { throw new IllegalStateException("Unknown state: " + state); - } + } + }; return newState; } @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) @@ -215,7 +177,7 @@ final class @Model.ParserClassName { @:{ if (transition.LookAheadHint != null) { - @:if (lookahead_@(transition.LookAheadHint.Id)(context, token)) + @:if (lookahead_@(transition.LookAheadHint.Id)(context)) @:{ } foreach(var production in transition.Productions) @@ -237,7 +199,7 @@ final class @Model.ParserClassName { @foreach(var lookAheadHint in Model.RuleSet.LookAheadHints) { - private boolean lookahead_@(lookAheadHint.Id)(ParserContext context, Token currentToken) { + private boolean lookahead_@(lookAheadHint.Id)(ParserContext context) { Token token; Queue queue = new ArrayDeque(); boolean match = false; @@ -272,9 +234,13 @@ final class @Model.ParserClassName { interface Builder { void build(Token token); + void startRule(RuleType ruleType); + void endRule(RuleType ruleType); + T getResult(); + void reset(String uri); } @@ -282,7 +248,43 @@ final class @Model.ParserClassName { @foreach(var rule in Model.RuleSet.TokenRules) { @:boolean match_@(rule.Name.Replace("#", ""))(Token token); + @: } void reset(); } + + static final class ParserContext { + final TokenScanner tokenScanner; + final TokenMatcher tokenMatcher; + final Queue tokenQueue; + final List errors; + + ParserContext(TokenScanner tokenScanner, TokenMatcher tokenMatcher, Queue tokenQueue, List errors) { + this.tokenScanner = tokenScanner; + this.tokenMatcher = tokenMatcher; + this.tokenQueue = tokenQueue; + this.errors = errors; + } + } + + enum TokenType { + None(RuleType.None), + @foreach(var rule in Model.RuleSet.TokenRules) + { @(rule.Name.Replace("#", ""))(RuleType.@(rule.Name.Replace("#", "_"))), +} ; + + final RuleType ruleType; + + TokenType(RuleType ruleType) { + this.ruleType = ruleType; + } + } + + enum RuleType { + None, + @foreach(var rule in Model.RuleSet.Where(r => !r.TempRule)) + { /** @rule.ToString(true) **/ + @rule.Name.Replace("#", "_"), +} + } } diff --git a/java/pom.xml b/java/pom.xml index 8057647e1..5e83f8936 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,14 +1,15 @@ - + 4.0.0 io.cucumber cucumber-parent - 4.5.0 + 5.0.0-SNAPSHOT gherkin - 37.0.1-SNAPSHOT + 38.0.0-SNAPSHOT jar Gherkin Gherkin parser @@ -56,7 +57,7 @@ io.cucumber messages - [31.0.0,32.0.0) + [32.0.0-SNAPSHOT,33.0.0) @@ -102,7 +103,7 @@ 1.37 test - + org.openjdk.jmh jmh-generator-annprocess @@ -191,7 +192,7 @@ ${project.build.directory}/codegen-classes - Generate + io.cucumber.gherkin.dialects.Generate ${project.build.directory}/generated-sources/gherkin io/cucumber/gherkin @@ -218,6 +219,22 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + validate + + check + + + checkstyle-suppressions.xml + + + + diff --git a/java/src/codegen/java/Generate.java b/java/src/codegen/java/io/cucumber/gherkin/dialects/Generate.java similarity index 91% rename from java/src/codegen/java/Generate.java rename to java/src/codegen/java/io/cucumber/gherkin/dialects/Generate.java index 7e302cf1d..db76fa914 100644 --- a/java/src/codegen/java/Generate.java +++ b/java/src/codegen/java/io/cucumber/gherkin/dialects/Generate.java @@ -1,3 +1,5 @@ +package io.cucumber.gherkin.dialects; + public class Generate { public static void main(String[] args) throws Exception { diff --git a/java/src/codegen/java/GenerateGherkinDialects.java b/java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateGherkinDialects.java similarity index 98% rename from java/src/codegen/java/GenerateGherkinDialects.java rename to java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateGherkinDialects.java index bae73c1b5..81b013c77 100644 --- a/java/src/codegen/java/GenerateGherkinDialects.java +++ b/java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateGherkinDialects.java @@ -1,3 +1,5 @@ +package io.cucumber.gherkin.dialects; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import freemarker.template.Configuration; diff --git a/java/src/codegen/java/GenerateKeywordMatchers.java b/java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateKeywordMatchers.java similarity index 83% rename from java/src/codegen/java/GenerateKeywordMatchers.java rename to java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateKeywordMatchers.java index 723ab0c9f..aaf638334 100644 --- a/java/src/codegen/java/GenerateKeywordMatchers.java +++ b/java/src/codegen/java/io/cucumber/gherkin/dialects/GenerateKeywordMatchers.java @@ -1,3 +1,5 @@ +package io.cucumber.gherkin.dialects; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import freemarker.template.Configuration; @@ -21,7 +23,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.function.BinaryOperator; import java.util.stream.Collectors; import static io.cucumber.messages.types.StepKeywordType.ACTION; @@ -35,6 +36,7 @@ import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.util.Comparator.naturalOrder; import static java.util.Map.Entry.comparingByKey; +import static java.util.Objects.requireNonNull; /* * This class generates the KeywordMatchers class using the FreeMarker @@ -74,7 +76,7 @@ private static Template readTemplate() throws IOException { private static Map readGherkinLanguages() throws IOException { ObjectMapper objectMapper = new ObjectMapper(); - TypeReference> mapObjectType = new TypeReference>() { + TypeReference> mapObjectType = new TypeReference<>() { }; try (Reader reader = newBufferedReader(Paths.get("../gherkin-languages.json"))) { Map sorted = new TreeMap<>(naturalOrder()); @@ -92,15 +94,13 @@ private static Map createMatcherModels(Map diale Map.Entry::getKey, entry -> matcherModel( entry.getKey(), (Map) entry.getValue()), - uniqueKeyCondition(), + GenerateKeywordMatchers::requireUniqueKeys, LinkedHashMap::new )); } - private static BinaryOperator uniqueKeyCondition() { - return (a, b) -> { - throw new IllegalStateException("Duplicate keys " + a + " and " + b); - }; + private static boolean requireUniqueKeys(Object a, Object b) { + throw new IllegalStateException("Duplicate keys " + a + " and " + b); } @SuppressWarnings("unchecked") @@ -110,7 +110,7 @@ private static Map matcherModel(String language, Map featureKeywords = distinctSortedKeywords( - (List) dialect.get("feature") + (List) requireNonNull(dialect.get("feature")) ); model.put("features", featureKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); @@ -120,7 +120,7 @@ private static Map matcherModel(String language, Map backgroundKeywords = distinctSortedKeywords( - (List) dialect.get("background") + (List) requireNonNull(dialect.get("background")) ); model.put("backgrounds", backgroundKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); @@ -130,7 +130,7 @@ private static Map matcherModel(String language, Map ruleKeywords = distinctSortedKeywords( - (List) dialect.get("rule") + (List) requireNonNull(dialect.get("rule")) ); model.put("rules", ruleKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); @@ -140,8 +140,8 @@ private static Map matcherModel(String language, Map scenarioKeywords = distinctSortedKeywords( - (List) dialect.get("scenario"), - (List) dialect.get("scenarioOutline") + (List) requireNonNull(dialect.get("scenario")), + (List) requireNonNull(dialect.get("scenarioOutline")) ); model.put("scenarios", scenarioKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); @@ -151,7 +151,7 @@ private static Map matcherModel(String language, Map exampleKeywords = distinctSortedKeywords( - (List) dialect.get("examples") + (List) requireNonNull(dialect.get("examples")) ); model.put("examples", exampleKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); @@ -161,24 +161,24 @@ private static Map matcherModel(String language, Map aggregateKeywordTypes = aggregateKeywordTypes( - (List) dialect.get("given"), - (List) dialect.get("when"), - (List) dialect.get("then"), - (List) dialect.get("and"), - (List) dialect.get("but") + (List) requireNonNull(dialect.get("given")), + (List) requireNonNull(dialect.get("when")), + (List) requireNonNull(dialect.get("then")), + (List) requireNonNull(dialect.get("and")), + (List) requireNonNull(dialect.get("but")) ); List stepKeywords = distinctSortedKeywords( - (List) dialect.get("given"), - (List) dialect.get("when"), - (List) dialect.get("then"), - (List) dialect.get("and"), - (List) dialect.get("but") + (List) requireNonNull(dialect.get("given")), + (List) requireNonNull(dialect.get("when")), + (List) requireNonNull(dialect.get("then")), + (List) requireNonNull(dialect.get("and")), + (List) requireNonNull(dialect.get("but")) ); model.put("steps", stepKeywords.stream().map(keyword -> { Map entry = new HashMap<>(); entry.put("keyword", keyword); entry.put("length", keyword.length()); - entry.put("keywordType", aggregateKeywordTypes.get(keyword).name()); + entry.put("keywordType", requireNonNull(aggregateKeywordTypes.get(keyword)).name()); return entry; }).collect(Collectors.toList())); @@ -186,11 +186,11 @@ private static Map matcherModel(String language, Map DIALECTS; private GherkinDialects() { - // Utility class + /* no-op */ } public static Set getLanguages() { diff --git a/java/src/codegen/resources/templates/keyword-matchers.java.ftl b/java/src/codegen/resources/io/cucumber/gherkin/dialects/templates/keyword-matchers.java.ftl similarity index 90% rename from java/src/codegen/resources/templates/keyword-matchers.java.ftl rename to java/src/codegen/resources/io/cucumber/gherkin/dialects/templates/keyword-matchers.java.ftl index 007c1c719..56f8f79fb 100644 --- a/java/src/codegen/resources/templates/keyword-matchers.java.ftl +++ b/java/src/codegen/resources/io/cucumber/gherkin/dialects/templates/keyword-matchers.java.ftl @@ -1,5 +1,7 @@ package io.cucumber.gherkin; +import org.jspecify.annotations.Nullable; + import java.util.Objects; import static io.cucumber.messages.types.StepKeywordType.ACTION; @@ -15,20 +17,20 @@ import static io.cucumber.gherkin.Constants.TITLE_KEYWORD_SEPARATOR_LENGTH; */ final class KeywordMatchers { + @Nullable static KeywordMatcher of(String language) { Objects.requireNonNull(language); - switch (language){ + return switch (language){ <#list matchers as name, matcher> - case "${name}": - return new ${matcher.className}(); + case "${name}" -> new ${matcher.className}(); - default: - return null; - } + default -> null; + }; } <#list matchers as name, matcher> private static final class ${matcher.className} implements KeywordMatcher { @Override + @Nullable public Match matchFeatureKeyword(Line line) { <#list matcher.features as feature> if (line.startsWithTitleKeyword("${feature.keyword}")) { @@ -38,6 +40,7 @@ final class KeywordMatchers { return null; } @Override + @Nullable public Match matchBackgroundKeyword(Line line) { <#list matcher.backgrounds as background> if (line.startsWithTitleKeyword("${background.keyword}")) { @@ -47,6 +50,7 @@ final class KeywordMatchers { return null; } @Override + @Nullable public Match matchRuleKeyword(Line line) { <#list matcher.rules as rule> if (line.startsWithTitleKeyword("${rule.keyword}")) { @@ -56,6 +60,7 @@ final class KeywordMatchers { return null; } @Override + @Nullable public Match matchScenarioKeyword(Line line) { <#list matcher.scenarios as scenario> if (line.startsWithTitleKeyword("${scenario.keyword}")) { @@ -65,6 +70,7 @@ final class KeywordMatchers { return null; } @Override + @Nullable public Match matchExampleKeyword(Line line) { <#list matcher.examples as example> if (line.startsWithTitleKeyword("${example.keyword}")) { @@ -74,6 +80,7 @@ final class KeywordMatchers { return null; } @Override + @Nullable public StepMatch matchStepKeyword(Line line) { <#list matcher.steps as step> if (line.startsWith("${step.keyword}")) { diff --git a/java/src/main/java/io/cucumber/gherkin/AstNode.java b/java/src/main/java/io/cucumber/gherkin/AstNode.java index 66858e0ea..f46dc59a7 100644 --- a/java/src/main/java/io/cucumber/gherkin/AstNode.java +++ b/java/src/main/java/io/cucumber/gherkin/AstNode.java @@ -1,5 +1,7 @@ package io.cucumber.gherkin; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; @@ -38,14 +40,15 @@ T getSingle(RuleType ruleType, T defaultResult) { return items == null ? defaultResult : items.get(0); } - @SuppressWarnings("unchecked") - T getSingle(RuleType ruleType) { + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + @Nullable T getSingle(RuleType ruleType) { // if not null, then at least one item is present because // the list was created in add(), so no need to check isEmpty() List items = (List) subItems.get(ruleType); return items == null ? null : items.get(0); } + @SuppressWarnings("TypeParameterUnusedInFormals") T getRequiredSingle(RuleType ruleType) { return requireNonNull(getSingle(ruleType)); } diff --git a/java/src/main/java/io/cucumber/gherkin/Constants.java b/java/src/main/java/io/cucumber/gherkin/Constants.java index 889391eec..909ecce01 100644 --- a/java/src/main/java/io/cucumber/gherkin/Constants.java +++ b/java/src/main/java/io/cucumber/gherkin/Constants.java @@ -2,6 +2,10 @@ final class Constants { + private Constants(){ + /* no-op */ + } + static final char TAG_PREFIX_CHAR = '@'; static final char COMMENT_PREFIX_CHAR = '#'; static final char TITLE_KEYWORD_SEPARATOR = ':'; diff --git a/java/src/main/java/io/cucumber/gherkin/EncodingParser.java b/java/src/main/java/io/cucumber/gherkin/EncodingParser.java index af378f039..1d2ae3813 100644 --- a/java/src/main/java/io/cucumber/gherkin/EncodingParser.java +++ b/java/src/main/java/io/cucumber/gherkin/EncodingParser.java @@ -18,7 +18,10 @@ final class EncodingParser { private static final Pattern COMMENT_OR_EMPTY_LINE_PATTERN = Pattern.compile("^\\s*#|^\\s*$"); private static final Pattern ENCODING_PATTERN = Pattern.compile("^\\s*#\\s*encoding\\s*:\\s*([0-9a-zA-Z\\-]+)", CASE_INSENSITIVE); - private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("([^\\n\\r]+)[\\n\\r]"); + + private EncodingParser(){ + /* no-op */ + } static String readWithEncodingFromSource(byte[] source) { byte[] bomFreeSource = removeByteOrderMarker(source); @@ -42,11 +45,9 @@ private static byte[] removeByteOrderMarker(byte[] source) { } private static Optional parseEncodingPragma(String source) { - // TODO performance: replace Pattern.matcher by Java 11 Iterator lines = source.lines().iterator() : to be about 2x faster - // Optimization: search for lines instead of splitting - Matcher m2 = LINE_SPLIT_PATTERN.matcher(source); - while (m2.find()) { - String line = m2.group(1); + var lines = source.lines().iterator(); + while (lines.hasNext()) { + String line = lines.next(); if (!COMMENT_OR_EMPTY_LINE_PATTERN.matcher(line).find()) { return Optional.empty(); } diff --git a/java/src/main/java/io/cucumber/gherkin/GherkinDialect.java b/java/src/main/java/io/cucumber/gherkin/GherkinDialect.java index e4eea412b..cbb497d64 100644 --- a/java/src/main/java/io/cucumber/gherkin/GherkinDialect.java +++ b/java/src/main/java/io/cucumber/gherkin/GherkinDialect.java @@ -1,6 +1,7 @@ package io.cucumber.gherkin; import io.cucumber.messages.types.StepKeywordType; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Comparator; @@ -16,8 +17,6 @@ import static io.cucumber.messages.types.StepKeywordType.CONJUNCTION; import static io.cucumber.messages.types.StepKeywordType.CONTEXT; import static io.cucumber.messages.types.StepKeywordType.OUTCOME; -import static java.util.Collections.unmodifiableList; -import static java.util.Collections.unmodifiableSet; import static java.util.Objects.requireNonNull; /** @@ -87,7 +86,7 @@ private static List distinctKeywords(List... keywords) { } List sortedKeywords = new ArrayList<>(uniqueKeywords); sortedKeywords.sort(LONGEST_TO_SHORTEST_COMPARATOR); - return unmodifiableList(sortedKeywords); + return List.copyOf(sortedKeywords); } private static Map> aggregateKeywordTypes( @@ -102,7 +101,7 @@ private static Map> aggregateKeywordTypes( addStepKeywordsTypes(stepKeywordsTypes, ACTION, whenKeywords); addStepKeywordsTypes(stepKeywordsTypes, OUTCOME, thenKeywords); addStepKeywordsTypes(stepKeywordsTypes, CONJUNCTION, distinctKeywords(andKeywords, butKeywords)); - stepKeywordsTypes.replaceAll((keyword, stepKeywordTypes) -> unmodifiableSet(stepKeywordTypes)); + stepKeywordsTypes.replaceAll((keyword, stepKeywordTypes) -> Set.copyOf(stepKeywordTypes)); return stepKeywordsTypes; } @@ -154,7 +153,7 @@ public List getStepKeywords() { * @deprecated use {{@link #getStepKeywordTypesSet(String)}} instead. */ @Deprecated - public List getStepKeywordTypes(String keyword) { + public @Nullable List getStepKeywordTypes(String keyword) { Set stepKeywordTypes = stepKeywordsTypes.get(keyword); if (stepKeywordTypes == null) { return null; diff --git a/java/src/main/java/io/cucumber/gherkin/GherkinDocumentBuilder.java b/java/src/main/java/io/cucumber/gherkin/GherkinDocumentBuilder.java index b9f764211..5b0da6f24 100644 --- a/java/src/main/java/io/cucumber/gherkin/GherkinDocumentBuilder.java +++ b/java/src/main/java/io/cucumber/gherkin/GherkinDocumentBuilder.java @@ -28,6 +28,7 @@ import static io.cucumber.gherkin.Parser.Builder; import static io.cucumber.gherkin.Parser.RuleType; import static io.cucumber.gherkin.Parser.TokenType; +import static java.util.Objects.requireNonNull; final class GherkinDocumentBuilder implements Builder { private final List comments = new ArrayList<>(); @@ -48,17 +49,17 @@ public void reset(String uri) { stack.push(new AstNode(RuleType.None)); } - private AstNode currentNode() { - return stack.peek(); + private AstNode requiredCurrentNode() { + return requireNonNull(stack.peek()); } @Override public void build(Token token) { if (token.matchedType == TokenType.Comment) { - Comment comment = new Comment(token.location, token.matchedText); + Comment comment = new Comment(token.location, token.getRequiredMatchedText()); comments.add(comment); } else { - currentNode().add(token.matchedType.ruleType, token); + requiredCurrentNode().add(token.getRequiredMatchedType().ruleType, token); } } @@ -71,66 +72,67 @@ public void startRule(RuleType ruleType) { public void endRule(RuleType ruleType) { AstNode node = stack.pop(); Object transformedNode = getTransformedNode(node); - currentNode().add(node.ruleType, transformedNode); + requiredCurrentNode().add(node.ruleType, transformedNode); } private Object getTransformedNode(AstNode node) { - switch (node.ruleType) { - case Step: { + return switch (node.ruleType) { + case Step -> { Token stepLine = node.getToken(TokenType.StepLine); - return new Step( + yield new Step( stepLine.location, - stepLine.matchedKeyword, + stepLine.getRequiredMatchedKeyword(), stepLine.keywordType, - stepLine.matchedText, + stepLine.getRequiredMatchedText(), node.getSingle(RuleType.DocString), node.getSingle(RuleType.DataTable), idGenerator.newId() ); } - case DocString: { + case DocString -> { Token separatorToken = node.getTokens(TokenType.DocStringSeparator).get(0); - String mediaType = separatorToken.matchedText.isEmpty() ? null : separatorToken.matchedText; + String matchedText = separatorToken.getRequiredMatchedText(); + String mediaType = matchedText.isEmpty() ? null : matchedText; List lineTokens = node.getTokens(TokenType.Other); String content = joinMatchedText(lineTokens, lineTokens.size()); - return new DocString( + yield new DocString( separatorToken.location, mediaType, content, - separatorToken.matchedKeyword + separatorToken.getRequiredMatchedKeyword() ); } - case DataTable: { + case DataTable -> { List rows = getTableRows(node); - return new DataTable(rows.get(0).getLocation(), rows); + yield new DataTable(rows.get(0).getLocation(), rows); } - case Background: { + case Background -> { Token backgroundLine = node.getToken(TokenType.BackgroundLine); - return new Background( + yield new Background( backgroundLine.location, - backgroundLine.matchedKeyword, - backgroundLine.matchedText, + backgroundLine.getRequiredMatchedKeyword(), + backgroundLine.getRequiredMatchedText(), getDescription(node), getSteps(node), idGenerator.newId() ); } - case ScenarioDefinition: { + case ScenarioDefinition -> { AstNode scenarioNode = node.getRequiredSingle(RuleType.Scenario); Token scenarioLine = scenarioNode.getToken(TokenType.ScenarioLine); - return new Scenario( + yield new Scenario( scenarioLine.location, getTags(node), - scenarioLine.matchedKeyword, - scenarioLine.matchedText, + scenarioLine.getRequiredMatchedKeyword(), + scenarioLine.getRequiredMatchedText(), getDescription(scenarioNode), getSteps(scenarioNode), scenarioNode.getItems(RuleType.ExamplesDefinition), idGenerator.newId() ); } - case ExamplesDefinition: { + case ExamplesDefinition -> { AstNode examplesNode = node.getRequiredSingle(RuleType.Examples); Token examplesLine = examplesNode.getToken(TokenType.ExamplesLine); // rows is null when a Scenario Outline has no Examples table @@ -145,30 +147,28 @@ private Object getTransformedNode(AstNode node) { tableBody = Collections.emptyList(); } - return new Examples( + yield new Examples( examplesLine.location, getTags(node), - examplesLine.matchedKeyword, - examplesLine.matchedText, + examplesLine.getRequiredMatchedKeyword(), + examplesLine.getRequiredMatchedText(), getDescription(examplesNode), tableHeader, tableBody, idGenerator.newId() ); } - case ExamplesTable: { - return getTableRows(node); - } - case Description: { + case ExamplesTable -> getTableRows(node); + case Description -> { List lineTokens = node.getTokens(TokenType.Other); // Trim trailing empty lines int toIndex = lineTokens.size(); - while (toIndex > 0 && lineTokens.get(toIndex - 1).line.isEmpty()) { + while (toIndex > 0 && lineTokens.get(toIndex - 1).getRequiredLine().isEmpty()) { toIndex--; } - return joinMatchedText(lineTokens, toIndex); + yield joinMatchedText(lineTokens, toIndex); } - case Feature: { + case Feature -> { AstNode header = node.getRequiredSingle(RuleType.FeatureHeader); List tags = getTags(header); Token featureLine = header.getToken(TokenType.FeatureLine); @@ -186,17 +186,17 @@ private Object getTransformedNode(AstNode node) { children.add(new FeatureChild(rule, null, null)); } - return new Feature( + yield new Feature( featureLine.location, tags, - featureLine.matchedLanguage, - featureLine.matchedKeyword, - featureLine.matchedText, + featureLine.getRequiredMatchedLanguage(), + featureLine.getRequiredMatchedKeyword(), + featureLine.getRequiredMatchedText(), getDescription(header), children ); } - case Rule: { + case Rule -> { AstNode header = node.getRequiredSingle(RuleType.RuleHeader); Token ruleLine = header.getToken(TokenType.RuleLine); @@ -213,45 +213,47 @@ private Object getTransformedNode(AstNode node) { children.add(new RuleChild(null, scenario)); } - return new Rule( + yield new Rule( ruleLine.location, tags, - ruleLine.matchedKeyword, - ruleLine.matchedText, + ruleLine.getRequiredMatchedKeyword(), + ruleLine.getRequiredMatchedText(), getDescription(header), children, idGenerator.newId() ); - } - case GherkinDocument: { + case GherkinDocument -> { Feature feature = node.getSingle(RuleType.Feature); // feature is null when the file is empty or contains only comments/whitespace, or no Cucumber feature - return new GherkinDocument(uri, feature, comments); + yield new GherkinDocument(uri, feature, comments); } - - } - return node; + // Do nothing. + default -> node; + }; } private static String joinMatchedText(List lineTokens, int toIndex) { StringBuilder content = new StringBuilder(FEATURE_FILE_AVERAGE_LINE_LENGTH * lineTokens.size()); for (int i = 0; i < toIndex; i++) { Token lineToken = lineTokens.get(i); - content.append(lineToken.matchedText).append('\n'); + String matchedText = lineToken.getRequiredMatchedText(); + content.append(matchedText).append('\n'); } int contentLength = content.length(); if (contentLength > 0) { - content.setLength(contentLength - 1); // Remove the last \n + // Remove the last \n + content.setLength(contentLength - 1); } return content.toString(); } - @SuppressWarnings("ForLoopReplaceableByForEach") // classic 'for' loop is ~2x faster than 'for-each' + @SuppressWarnings("ForLoopReplaceableByForEach") private List getTableRows(AstNode node) { List tokens = node.getTokens(TokenType.TableRow); int tokenSize = tokens.size(); List rows = new ArrayList<>(tokenSize); + // classic 'for' loop is ~2x faster than 'for-each' for (int i = 0; i < tokenSize; i++) { Token token = tokens.get(i); rows.add(new TableRow(token.location, getCells(token), idGenerator.newId())); @@ -260,12 +262,13 @@ private List getTableRows(AstNode node) { return rows; } - @SuppressWarnings("ForLoopReplaceableByForEach") // classic 'for' loop is ~2x faster than 'for-each' + @SuppressWarnings("ForLoopReplaceableByForEach") private void ensureCellCount(List rows) { if (rows.isEmpty()) return; int firstRowCellsSize = rows.get(0).getCells().size(); + // classic 'for' loop is ~2x faster than 'for-each' for (int i = 0, rowsSize = rows.size(); i < rowsSize; i++) { TableRow row = rows.get(i); if (row.getCells().size() != firstRowCellsSize) { @@ -274,11 +277,15 @@ private void ensureCellCount(List rows) { } } - @SuppressWarnings("ForLoopReplaceableByForEach") // classic 'for' loop is ~2x faster than 'for-each' + @SuppressWarnings("ForLoopReplaceableByForEach") private List getCells(Token token) { List matchedItems = token.matchedItems; + if (matchedItems == null) { + return new ArrayList<>(0); + } int itemSize = matchedItems.size(); List cells = new ArrayList<>(itemSize); + // classic 'for' loop is ~2x faster than 'for-each' for (int i = 0; i < itemSize; i++) { LineSpan cellItem = matchedItems.get(i); TableCell tableCell = new TableCell( @@ -300,15 +307,19 @@ private String getDescription(AstNode node) { private List getTags(AstNode node) { AstNode tagsNode = node.getSingle(RuleType.Tags); - if (tagsNode == null) {// tags are optional - return Collections.emptyList(); + // tags are optional + if (tagsNode == null) { + return new ArrayList<>(0); } List tokens = tagsNode.getTokens(TokenType.TagLine); List tags = new ArrayList<>(); for (Token token : tokens) { - for (LineSpan tagItem : token.matchedItems) { - tags.add(new Tag(atColumn(token.location, tagItem.column), tagItem.text, idGenerator.newId())); + List matchedItems = token.matchedItems; + if (matchedItems != null) { + for (LineSpan tagItem : matchedItems) { + tags.add(new Tag(atColumn(token.location, tagItem.column), tagItem.text, idGenerator.newId())); + } } } return tags; @@ -316,7 +327,7 @@ private List getTags(AstNode node) { @Override public GherkinDocument getResult() { - return currentNode().getSingle(RuleType.GherkinDocument); + return requiredCurrentNode().getRequiredSingle(RuleType.GherkinDocument); } } diff --git a/java/src/main/java/io/cucumber/gherkin/GherkinParser.java b/java/src/main/java/io/cucumber/gherkin/GherkinParser.java index 522f74d9b..d89c5086d 100644 --- a/java/src/main/java/io/cucumber/gherkin/GherkinParser.java +++ b/java/src/main/java/io/cucumber/gherkin/GherkinParser.java @@ -21,7 +21,7 @@ import static io.cucumber.gherkin.EncodingParser.readWithEncodingFromSource; import static io.cucumber.messages.types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toList; /** * Main entry point for the Gherkin library @@ -32,7 +32,7 @@ public final class GherkinParser { * Estimate of the average line length in a feature file. */ static final int FEATURE_FILE_AVERAGE_LINE_LENGTH = 100; - + /** * Estimate of the average number of lines in a feature file. */ @@ -43,42 +43,6 @@ public final class GherkinParser { */ static final int FEATURE_FILE_AVERAGE_SIZE = FEATURE_FILE_AVERAGE_LINE_LENGTH * FEATURE_FILE_AVERAGE_LINE_COUNT; - public static final class Builder { - private boolean includeSource = true; - private boolean includeGherkinDocument = true; - private boolean includePickles = true; - private IdGenerator idGenerator = () -> UUID.randomUUID().toString(); - - private Builder() { - - } - - public Builder includeSource(boolean includeSource) { - this.includeSource = includeSource; - return this; - } - - public Builder includeGherkinDocument(boolean includeGherkinDocument) { - this.includeGherkinDocument = includeGherkinDocument; - return this; - } - - public Builder includePickles(boolean includePickles) { - this.includePickles = includePickles; - return this; - } - - public Builder idGenerator(IdGenerator idGenerator) { - this.idGenerator = requireNonNull(idGenerator); - return this; - } - - public GherkinParser build() { - return new GherkinParser(includeSource, includeGherkinDocument, includePickles, idGenerator); - } - - } - private final boolean includeSource; private final boolean includeGherkinDocument; private final boolean includePickles; @@ -86,7 +50,7 @@ public GherkinParser build() { private final PickleCompiler pickleCompiler; private GherkinParser(boolean includeSource, boolean includeGherkinDocument, boolean includePickles, - IdGenerator idGenerator) { + IdGenerator idGenerator) { this.includeSource = includeSource; this.includeGherkinDocument = includeGherkinDocument; this.includePickles = includePickles; @@ -107,7 +71,7 @@ public Stream parse(Path source) throws IOException { public Stream parse(String uri, InputStream source) throws IOException { requireNonNull(uri); requireNonNull(source); - return parse(uri, InputStreams.readAllBytes(source)); + return parse(uri, source.readAllBytes()); } public Stream parse(String uri, byte[] source) { @@ -141,7 +105,7 @@ private List parse(Source source) { return parse(source.getUri(), source.getData()); } - @SuppressWarnings("ForLoopReplaceableByForEach") // classic 'for' loop is ~2x faster than 'for-each' + @SuppressWarnings("ForLoopReplaceableByForEach") private List parse(String uri, String data) { List messages = new ArrayList<>(); GherkinDocumentBuilder documentBuilder = new GherkinDocumentBuilder(idGenerator, uri); @@ -154,15 +118,16 @@ private List parse(String uri, String data) { } if (includePickles) { List compile = pickleCompiler.compile(gherkinDocument, uri); + // classic 'for' loop is ~2x faster than 'for-each' for (int i = 0, compileSize = compile.size(); i < compileSize; i++) { Pickle pickle = compile.get(i); messages.add(Envelope.of(pickle)); } } } catch (CompositeParserException composite) { - composite.errors.stream() + messages.addAll(composite.errors.stream() .map(error -> createParseError(error, uri)) - .collect(toCollection(() -> messages)); + .collect(toList())); } catch (ParserException error) { messages.add(createParseError(error, uri)); } @@ -177,8 +142,44 @@ private Envelope createParseError(ParserException e, String uri) { null, e.location ), - e.getMessage() + // ParserException always has a message + requireNonNull(e.getMessage()) )); } + public static final class Builder { + private boolean includeSource = true; + private boolean includeGherkinDocument = true; + private boolean includePickles = true; + private IdGenerator idGenerator = () -> UUID.randomUUID().toString(); + + private Builder() { + + } + + public Builder includeSource(boolean includeSource) { + this.includeSource = includeSource; + return this; + } + + public Builder includeGherkinDocument(boolean includeGherkinDocument) { + this.includeGherkinDocument = includeGherkinDocument; + return this; + } + + public Builder includePickles(boolean includePickles) { + this.includePickles = includePickles; + return this; + } + + public Builder idGenerator(IdGenerator idGenerator) { + this.idGenerator = requireNonNull(idGenerator); + return this; + } + + public GherkinParser build() { + return new GherkinParser(includeSource, includeGherkinDocument, includePickles, idGenerator); + } + + } } diff --git a/java/src/main/java/io/cucumber/gherkin/GherkinTokenMatcher.java b/java/src/main/java/io/cucumber/gherkin/GherkinTokenMatcher.java index bba76eac0..c7307334b 100644 --- a/java/src/main/java/io/cucumber/gherkin/GherkinTokenMatcher.java +++ b/java/src/main/java/io/cucumber/gherkin/GherkinTokenMatcher.java @@ -3,6 +3,7 @@ import io.cucumber.gherkin.Parser.TokenMatcher; import io.cucumber.messages.types.Location; import io.cucumber.messages.types.StepKeywordType; +import org.jspecify.annotations.Nullable; import java.util.HashMap; import java.util.List; @@ -27,13 +28,14 @@ final class GherkinTokenMatcher implements TokenMatcher { private final String defaultLanguage; // Expect at most two languages, the default language and one other private final Map activeKeywordMatchers = new HashMap<>(2); - private String currentLanguage; + private @Nullable String currentLanguage; private KeywordMatcher currentKeywordMatcher; - private String activeDocStringSeparator = null; + private @Nullable String activeDocStringSeparator = null; private int indentToRemove = 0; GherkinTokenMatcher(String defaultLanguage) { this.defaultLanguage = defaultLanguage; + this.currentKeywordMatcher = requireKeywordMatcher(defaultLanguage, new Location(0, 0)); reset(); } @@ -43,25 +45,31 @@ final class GherkinTokenMatcher implements TokenMatcher { @Override public void reset() { - // TODO performance: reset() is called once in the constructor and once for each file (Parser.parse()). It could be called only once, but there is no measurable impact with the profiler + // TODO performance: reset() is called once in the constructor and once for each file (Parser.parse()). + // It could be called only once, but there is no measurable impact with the profiler activeDocStringSeparator = null; indentToRemove = 0; setLanguageMatched(defaultLanguage, null); } - private void setLanguageMatched(String language, Location location) { + private void setLanguageMatched(String language, @Nullable Location location) { if (language.equals(currentLanguage)) { return; } + KeywordMatcher keywordMatcher = requireKeywordMatcher(language, location); + currentLanguage = language; + currentKeywordMatcher = keywordMatcher; + } + + private KeywordMatcher requireKeywordMatcher(String language, @Nullable Location location) { KeywordMatcher keywordMatcher = activeKeywordMatchers.computeIfAbsent(language, KeywordMatchers::of); if (keywordMatcher == null) { throw new ParserException.NoSuchLanguageException(language, location); } - currentLanguage = language; - currentKeywordMatcher = keywordMatcher; + return keywordMatcher; } - private void setTokenMatched(Token token, TokenType matchedType, String text, String keyword, int indent, StepKeywordType keywordType, List items) { + private void setTokenMatched(Token token, TokenType matchedType, @Nullable String text, @Nullable String keyword, int indent, @Nullable StepKeywordType keywordType, @Nullable List items) { token.matchedType = matchedType; token.matchedKeyword = keyword; token.keywordType = keywordType; @@ -90,7 +98,7 @@ public boolean match_Other(Token token) { @Override public boolean match_Empty(Token token) { - if (!token.line.isEmpty()) { + if (!token.getRequiredLine().isEmpty()) { return false; } setTokenMatched(token, TokenType.Empty, null, null, 0, null, null); @@ -99,36 +107,39 @@ public boolean match_Empty(Token token) { @Override public boolean match_Comment(Token token) { - if (!token.line.startsWith(COMMENT_PREFIX_CHAR)) { + Line line = token.getRequiredLine(); + if (!line.startsWith(COMMENT_PREFIX_CHAR)) { return false; } - String text = token.line.getRawText(); + String text = line.getRawText(); setTokenMatched(token, TokenType.Comment, text, null, 0, null, null); return true; } @Override public boolean match_Language(Token token) { - if (!token.line.startsWith(COMMENT_PREFIX_CHAR)) { + Line line = token.getRequiredLine(); + if (!line.startsWith(COMMENT_PREFIX_CHAR)) { return false; } - Matcher matcher = LANGUAGE_PATTERN.matcher(token.line.getText()); + Matcher matcher = LANGUAGE_PATTERN.matcher(line.getText()); if (!matcher.matches()) { return false; } String language = matcher.group(1); - setTokenMatched(token, TokenType.Language, language, null, token.line.getIndent(), null, null); + setTokenMatched(token, TokenType.Language, language, null, line.getIndent(), null, null); setLanguageMatched(language, token.location); return true; } @Override public boolean match_TagLine(Token token) { - if (!token.line.startsWith(TAG_PREFIX_CHAR)) { + Line line = token.getRequiredLine(); + if (!line.startsWith(TAG_PREFIX_CHAR)) { return false; } - List tags = TagLine.parse(token.line.getIndent(), token.line.getText(), token.location); - setTokenMatched(token, TokenType.TagLine, null, null, token.line.getIndent(), null, tags); + List tags = TagLine.parse(line.getIndent(), line.getText(), token.location); + setTokenMatched(token, TokenType.TagLine, null, null, line.getIndent(), null, tags); return true; } @@ -157,15 +168,16 @@ public boolean match_ExamplesLine(Token token) { return matchTitleLine(token, TokenType.ExamplesLine, currentKeywordMatcher::matchExampleKeyword); } - private boolean matchTitleLine(Token token, TokenType tokenType, Function matcher) { - KeywordMatcher.Match match = matcher.apply(token.line); + private boolean matchTitleLine(Token token, TokenType tokenType, Function matcher) { + Line line = token.getRequiredLine(); + KeywordMatcher.Match match = matcher.apply(line); if (match == null) { return false; } String keyword = match.getKeyword(); int keywordLength = match.getKeywordLength(); - String title = token.line.substringTrimmed(keywordLength); - setTokenMatched(token, tokenType, title, keyword, token.line.getIndent(), null, null); + String title = line.substringTrimmed(keywordLength); + setTokenMatched(token, tokenType, title, keyword, line.getIndent(), null, null); return true; } @@ -180,56 +192,60 @@ public boolean match_DocStringSeparator(Token token) { } private boolean match_DocStringSeparator(Token token, String separator, boolean isOpen) { - if (!token.line.startsWith(separator)) { + Line line = token.getRequiredLine(); + if (!line.startsWith(separator)) { return false; } String mediaType = null; if (isOpen) { - mediaType = token.line.substringTrimmed(separator.length()); + mediaType = line.substringTrimmed(separator.length()); activeDocStringSeparator = separator; - indentToRemove = token.line.getIndent(); + indentToRemove = line.getIndent(); } else { activeDocStringSeparator = null; indentToRemove = 0; } - setTokenMatched(token, TokenType.DocStringSeparator, mediaType, separator, token.line.getIndent(), null, null); + setTokenMatched(token, TokenType.DocStringSeparator, mediaType, separator, line.getIndent(), null, null); return true; } @Override public boolean match_StepLine(Token token) { - KeywordMatcher.StepMatch match = currentKeywordMatcher.matchStepKeyword(token.line); + Line line = token.getRequiredLine(); + KeywordMatcher.StepMatch match = currentKeywordMatcher.matchStepKeyword(line); if (match == null) { return false; } String keyword = match.getKeyword(); int keywordLength = match.getKeywordLength(); StepKeywordType keywordType = match.getKeywordType(); - String stepText = token.line.substringTrimmed(keywordLength); - setTokenMatched(token, TokenType.StepLine, stepText, keyword, token.line.getIndent(), keywordType, null); + String stepText = line.substringTrimmed(keywordLength); + setTokenMatched(token, TokenType.StepLine, stepText, keyword, line.getIndent(), keywordType, null); return true; } @Override public boolean match_TableRow(Token token) { - if (!token.line.startsWith(TABLE_CELL_SEPARATOR)) { + Line line = token.getRequiredLine(); + if (!line.startsWith(TABLE_CELL_SEPARATOR)) { return false; } - List tableCells = TableRowLine.parse(token.line.getIndent(), token.line.getText()); - setTokenMatched(token, TokenType.TableRow, null, null, token.line.getIndent(), null, tableCells); + List tableCells = TableRowLine.parse(line.getIndent(), line.getText()); + setTokenMatched(token, TokenType.TableRow, null, null, line.getIndent(), null, tableCells); return true; } private String removeDocStringIndent(Token token) { + Line line = token.getRequiredLine(); if (activeDocStringSeparator == null) { - return token.line.getRawText(); + return line.getRawText(); } - if (indentToRemove > token.line.getIndent()) { + if (indentToRemove > line.getIndent()) { // BUG: Removes trailing whitespace! - return token.line.getText(); + return line.getText(); } - return token.line.getRawTextSubstring(indentToRemove); + return line.getRawTextSubstring(indentToRemove); } private String unescapeDocString(String text) { diff --git a/java/src/main/java/io/cucumber/gherkin/InputStreams.java b/java/src/main/java/io/cucumber/gherkin/InputStreams.java deleted file mode 100644 index 4d896bc4a..000000000 --- a/java/src/main/java/io/cucumber/gherkin/InputStreams.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.cucumber.gherkin; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -final class InputStreams { - - /** - * Shim for InputStream.readAllBytes - */ - static byte[] readAllBytes(InputStream source) throws IOException { - final byte[] buffer = new byte[2 * 1024]; // 2KB - int read; - // The BAOS initial capacity is guessed from the average line count - // and average line length, to avoid resizing the underlying array. - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(GherkinParser.FEATURE_FILE_AVERAGE_SIZE); - while (-1 != (read = source.read(buffer, 0, buffer.length))) { - outputStream.write(buffer, 0, read); - } - return outputStream.toByteArray(); - } - -} diff --git a/java/src/main/java/io/cucumber/gherkin/KeywordMatcher.java b/java/src/main/java/io/cucumber/gherkin/KeywordMatcher.java index 004a06383..f364c7522 100644 --- a/java/src/main/java/io/cucumber/gherkin/KeywordMatcher.java +++ b/java/src/main/java/io/cucumber/gherkin/KeywordMatcher.java @@ -1,15 +1,21 @@ package io.cucumber.gherkin; import io.cucumber.messages.types.StepKeywordType; +import org.jspecify.annotations.Nullable; interface KeywordMatcher { - Match matchFeatureKeyword(Line line); - Match matchBackgroundKeyword(Line line); - Match matchRuleKeyword(Line line); - Match matchScenarioKeyword(Line line); - Match matchExampleKeyword(Line line); - StepMatch matchStepKeyword(Line line); + @Nullable Match matchFeatureKeyword(Line line); + + @Nullable Match matchBackgroundKeyword(Line line); + + @Nullable Match matchRuleKeyword(Line line); + + @Nullable Match matchScenarioKeyword(Line line); + + @Nullable Match matchExampleKeyword(Line line); + + @Nullable StepMatch matchStepKeyword(Line line); final class StepMatch { private final String keyword; @@ -29,13 +35,13 @@ String getKeyword() { int getKeywordLength() { return keywordLength; } - + StepKeywordType getKeywordType() { return keywordType; } } - + final class Match { private final String keyword; private final int keywordLength; diff --git a/java/src/main/java/io/cucumber/gherkin/LineSpan.java b/java/src/main/java/io/cucumber/gherkin/LineSpan.java index 406240488..4352a3f82 100644 --- a/java/src/main/java/io/cucumber/gherkin/LineSpan.java +++ b/java/src/main/java/io/cucumber/gherkin/LineSpan.java @@ -3,11 +3,11 @@ final class LineSpan { /** * Index-1 based position in codepoints. - */ + */ final int column; /** - * Text part of the line + * Text part of the line */ final String text; diff --git a/java/src/main/java/io/cucumber/gherkin/Locations.java b/java/src/main/java/io/cucumber/gherkin/Locations.java index 11d875f0c..1601d993f 100644 --- a/java/src/main/java/io/cucumber/gherkin/Locations.java +++ b/java/src/main/java/io/cucumber/gherkin/Locations.java @@ -15,15 +15,19 @@ final class Locations { * We can't use Long.valueOf() because it caches only the first 128 * values, and typical feature files have much more lines. */ - private static final Long[] longs = new Long[4096]; + private static final Integer[] ints = new Integer[4096]; static { - for (int i = 0; i < longs.length; i++) { - longs[i] = (long) i; + for (int i = 0; i < ints.length; i++) { + ints[i] = i; } } + + private Locations(){ + /* no-op */ + } - private static Long getLong(int i) { + private static Integer getInteger(int i) { // JMH benchmark shows that this implementation is the // fastest when i<4096 (and about 20% slower than // Long.valueOf() when i>=4096). @@ -37,21 +41,21 @@ private static Long getLong(int i) { // 1024/2048/4096 initial size // - dynamic lazy initialized cache with 256 // initialized size - if (i >= longs.length) { - return (long) i; + if (i >= ints.length) { + return i; } - return longs[i]; + return ints[i]; } static Location atColumn(Location location, int column) { // By design, the location cannot be null (it comes from the Token) // By design, the column cannot be less than 1 - return new Location(location.getLine(), getLong(column)); + return new Location(location.getLine(), getInteger(column)); } static Location atLine(int line) { // By design, the column cannot be less than 1 (it comes from TokenScanner) - return new Location(getLong(line), null); + return new Location(getInteger(line), null); } } diff --git a/java/src/main/java/io/cucumber/gherkin/Parser.java b/java/src/main/java/io/cucumber/gherkin/Parser.java index 0d3057392..f599623ef 100644 --- a/java/src/main/java/io/cucumber/gherkin/Parser.java +++ b/java/src/main/java/io/cucumber/gherkin/Parser.java @@ -2,99 +2,21 @@ import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Queue; import java.util.function.BiConsumer; import java.util.function.BiPredicate; -import static java.util.Arrays.asList; - /** * This class was generated by Berp. *

* Changes to this class will be lost if the code is regenerated. */ final class Parser { - enum TokenType { - None(RuleType.None), - EOF(RuleType._EOF), - Empty(RuleType._Empty), - Comment(RuleType._Comment), - TagLine(RuleType._TagLine), - FeatureLine(RuleType._FeatureLine), - RuleLine(RuleType._RuleLine), - BackgroundLine(RuleType._BackgroundLine), - ScenarioLine(RuleType._ScenarioLine), - ExamplesLine(RuleType._ExamplesLine), - StepLine(RuleType._StepLine), - DocStringSeparator(RuleType._DocStringSeparator), - TableRow(RuleType._TableRow), - Language(RuleType._Language), - Other(RuleType._Other), - ; - - final RuleType ruleType; - - TokenType(RuleType ruleType) { - this.ruleType = ruleType; - } - } - - enum RuleType { - None, - _EOF, // #EOF - _Empty, // #Empty - _Comment, // #Comment - _TagLine, // #TagLine - _FeatureLine, // #FeatureLine - _RuleLine, // #RuleLine - _BackgroundLine, // #BackgroundLine - _ScenarioLine, // #ScenarioLine - _ExamplesLine, // #ExamplesLine - _StepLine, // #StepLine - _DocStringSeparator, // #DocStringSeparator - _TableRow, // #TableRow - _Language, // #Language - _Other, // #Other - GherkinDocument, // GherkinDocument! := Feature? - Feature, // Feature! := FeatureHeader Background? ScenarioDefinition* Rule* - FeatureHeader, // FeatureHeader! := #Language? Tags? #FeatureLine DescriptionHelper - Rule, // Rule! := RuleHeader Background? ScenarioDefinition* - RuleHeader, // RuleHeader! := Tags? #RuleLine DescriptionHelper - Background, // Background! := #BackgroundLine DescriptionHelper Step* - ScenarioDefinition, // ScenarioDefinition! [#Empty|#Comment|#TagLine->#ScenarioLine] := Tags? Scenario - Scenario, // Scenario! := #ScenarioLine DescriptionHelper Step* ExamplesDefinition* - ExamplesDefinition, // ExamplesDefinition! [#Empty|#Comment|#TagLine->#ExamplesLine] := Tags? Examples - Examples, // Examples! := #ExamplesLine DescriptionHelper ExamplesTable? - ExamplesTable, // ExamplesTable! := #TableRow #TableRow* - Step, // Step! := #StepLine StepArg? - StepArg, // StepArg := (DataTable | DocString) - DataTable, // DataTable! := #TableRow+ - DocString, // DocString! := #DocStringSeparator #Other* #DocStringSeparator - Tags, // Tags! := #TagLine+ - DescriptionHelper, // DescriptionHelper := #Empty* Description? - Description, // Description! := (#Other | #Comment)+ - ; - - } private final Builder builder; - static final class ParserContext { - final TokenScanner tokenScanner; - final TokenMatcher tokenMatcher; - final Queue tokenQueue; - final List errors; - - ParserContext(TokenScanner tokenScanner, TokenMatcher tokenMatcher, Queue tokenQueue, List errors) { - this.tokenScanner = tokenScanner; - this.tokenMatcher = tokenMatcher; - this.tokenQueue = tokenQueue; - this.errors = errors; - } - } - Parser(Builder builder) { this.builder = builder; } @@ -118,7 +40,7 @@ T parse(TokenScanner tokenScanner, TokenMatcher tokenMatcher, String uri) { ParserContext context = new ParserContext( tokenScanner, tokenMatcher, - new LinkedList<>(), + new ArrayDeque<>(), new ArrayList<>() ); @@ -142,7 +64,7 @@ T parse(TokenScanner tokenScanner, TokenMatcher tokenMatcher, String uri) { private void addError(ParserContext context, ParserException error) { String newErrorMessage = error.getMessage(); for (ParserException e : context.errors) { - if (e.getMessage().equals(newErrorMessage)) { + if (Objects.equals(e.getMessage(), newErrorMessage)) { return; } } @@ -263,137 +185,53 @@ private boolean match_Other(final ParserContext context, final Token token) { } private int matchToken(int state, Token token, ParserContext context) { - int newState; - switch (state) { - case 0: - newState = matchTokenAt_0(token, context); - break; - case 1: - newState = matchTokenAt_1(token, context); - break; - case 2: - newState = matchTokenAt_2(token, context); - break; - case 3: - newState = matchTokenAt_3(token, context); - break; - case 4: - newState = matchTokenAt_4(token, context); - break; - case 5: - newState = matchTokenAt_5(token, context); - break; - case 6: - newState = matchTokenAt_6(token, context); - break; - case 7: - newState = matchTokenAt_7(token, context); - break; - case 8: - newState = matchTokenAt_8(token, context); - break; - case 9: - newState = matchTokenAt_9(token, context); - break; - case 10: - newState = matchTokenAt_10(token, context); - break; - case 11: - newState = matchTokenAt_11(token, context); - break; - case 12: - newState = matchTokenAt_12(token, context); - break; - case 13: - newState = matchTokenAt_13(token, context); - break; - case 14: - newState = matchTokenAt_14(token, context); - break; - case 15: - newState = matchTokenAt_15(token, context); - break; - case 16: - newState = matchTokenAt_16(token, context); - break; - case 17: - newState = matchTokenAt_17(token, context); - break; - case 18: - newState = matchTokenAt_18(token, context); - break; - case 19: - newState = matchTokenAt_19(token, context); - break; - case 20: - newState = matchTokenAt_20(token, context); - break; - case 21: - newState = matchTokenAt_21(token, context); - break; - case 22: - newState = matchTokenAt_22(token, context); - break; - case 23: - newState = matchTokenAt_23(token, context); - break; - case 24: - newState = matchTokenAt_24(token, context); - break; - case 25: - newState = matchTokenAt_25(token, context); - break; - case 26: - newState = matchTokenAt_26(token, context); - break; - case 27: - newState = matchTokenAt_27(token, context); - break; - case 28: - newState = matchTokenAt_28(token, context); - break; - case 29: - newState = matchTokenAt_29(token, context); - break; - case 30: - newState = matchTokenAt_30(token, context); - break; - case 31: - newState = matchTokenAt_31(token, context); - break; - case 32: - newState = matchTokenAt_32(token, context); - break; - case 33: - newState = matchTokenAt_33(token, context); - break; - case 35: - newState = matchTokenAt_35(token, context); - break; - case 36: - newState = matchTokenAt_36(token, context); - break; - case 37: - newState = matchTokenAt_37(token, context); - break; - case 38: - newState = matchTokenAt_38(token, context); - break; - case 39: - newState = matchTokenAt_39(token, context); - break; - case 40: - newState = matchTokenAt_40(token, context); - break; - case 41: - newState = matchTokenAt_41(token, context); - break; - case 42: - newState = matchTokenAt_42(token, context); - break; - default: + int newState = switch (state) { + case 0 -> matchTokenAt_0(token, context); + case 1 -> matchTokenAt_1(token, context); + case 2 -> matchTokenAt_2(token, context); + case 3 -> matchTokenAt_3(token, context); + case 4 -> matchTokenAt_4(token, context); + case 5 -> matchTokenAt_5(token, context); + case 6 -> matchTokenAt_6(token, context); + case 7 -> matchTokenAt_7(token, context); + case 8 -> matchTokenAt_8(token, context); + case 9 -> matchTokenAt_9(token, context); + case 10 -> matchTokenAt_10(token, context); + case 11 -> matchTokenAt_11(token, context); + case 12 -> matchTokenAt_12(token, context); + case 13 -> matchTokenAt_13(token, context); + case 14 -> matchTokenAt_14(token, context); + case 15 -> matchTokenAt_15(token, context); + case 16 -> matchTokenAt_16(token, context); + case 17 -> matchTokenAt_17(token, context); + case 18 -> matchTokenAt_18(token, context); + case 19 -> matchTokenAt_19(token, context); + case 20 -> matchTokenAt_20(token, context); + case 21 -> matchTokenAt_21(token, context); + case 22 -> matchTokenAt_22(token, context); + case 23 -> matchTokenAt_23(token, context); + case 24 -> matchTokenAt_24(token, context); + case 25 -> matchTokenAt_25(token, context); + case 26 -> matchTokenAt_26(token, context); + case 27 -> matchTokenAt_27(token, context); + case 28 -> matchTokenAt_28(token, context); + case 29 -> matchTokenAt_29(token, context); + case 30 -> matchTokenAt_30(token, context); + case 31 -> matchTokenAt_31(token, context); + case 32 -> matchTokenAt_32(token, context); + case 33 -> matchTokenAt_33(token, context); + case 35 -> matchTokenAt_35(token, context); + case 36 -> matchTokenAt_36(token, context); + case 37 -> matchTokenAt_37(token, context); + case 38 -> matchTokenAt_38(token, context); + case 39 -> matchTokenAt_39(token, context); + case 40 -> matchTokenAt_40(token, context); + case 41 -> matchTokenAt_41(token, context); + case 42 -> matchTokenAt_42(token, context); + default -> { throw new IllegalStateException("Unknown state: " + state); - } + } + }; return newState; } @@ -438,7 +276,7 @@ private int matchTokenAt_0(Token token, ParserContext context) { } final String stateComment = "State: 0 - Start"; - List expectedTokens = asList("#EOF", "#Language", "#TagLine", "#FeatureLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#Language", "#TagLine", "#FeatureLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -472,7 +310,7 @@ private int matchTokenAt_1(Token token, ParserContext context) { } final String stateComment = "State: 1 - GherkinDocument:0>Feature:0>FeatureHeader:0>#Language:0"; - List expectedTokens = asList("#TagLine", "#FeatureLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#FeatureLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -506,7 +344,7 @@ private int matchTokenAt_2(Token token, ParserContext context) { } final String stateComment = "State: 2 - GherkinDocument:0>Feature:0>FeatureHeader:1>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#FeatureLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#FeatureLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -544,7 +382,7 @@ private int matchTokenAt_3(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.FeatureHeader); startRule(context, RuleType.ScenarioDefinition); @@ -586,7 +424,7 @@ private int matchTokenAt_3(Token token, ParserContext context) { } final String stateComment = "State: 3 - GherkinDocument:0>Feature:0>FeatureHeader:2>#FeatureLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -620,7 +458,7 @@ private int matchTokenAt_4(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.FeatureHeader); @@ -665,7 +503,7 @@ private int matchTokenAt_4(Token token, ParserContext context) { } final String stateComment = "State: 4 - GherkinDocument:0>Feature:0>FeatureHeader:3>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -702,7 +540,7 @@ private int matchTokenAt_5(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Background); startRule(context, RuleType.ScenarioDefinition); @@ -744,7 +582,7 @@ private int matchTokenAt_5(Token token, ParserContext context) { } final String stateComment = "State: 5 - GherkinDocument:0>Feature:1>Background:0>#BackgroundLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -777,7 +615,7 @@ private int matchTokenAt_6(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Background); @@ -822,7 +660,7 @@ private int matchTokenAt_6(Token token, ParserContext context) { } final String stateComment = "State: 6 - GherkinDocument:0>Feature:1>Background:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -862,7 +700,7 @@ private int matchTokenAt_7(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Step); endRule(context, RuleType.Background); @@ -912,7 +750,7 @@ private int matchTokenAt_7(Token token, ParserContext context) { } final String stateComment = "State: 7 - GherkinDocument:0>Feature:1>Background:2>Step:0>#StepLine:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -947,7 +785,7 @@ private int matchTokenAt_8(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -1001,7 +839,7 @@ private int matchTokenAt_8(Token token, ParserContext context) { } final String stateComment = "State: 8 - GherkinDocument:0>Feature:1>Background:2>Step:1>StepArg:0>__alt0:0>DataTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1036,7 +874,7 @@ private int matchTokenAt_9(Token token, ParserContext context) { } final String stateComment = "State: 9 - GherkinDocument:0>Feature:2>ScenarioDefinition:0>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#ScenarioLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#ScenarioLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1074,7 +912,7 @@ private int matchTokenAt_10(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { startRule(context, RuleType.ExamplesDefinition); startRule(context, RuleType.Tags); @@ -1084,7 +922,7 @@ private int matchTokenAt_10(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Scenario); endRule(context, RuleType.ScenarioDefinition); @@ -1137,7 +975,7 @@ private int matchTokenAt_10(Token token, ParserContext context) { } final String stateComment = "State: 10 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:0>#ScenarioLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1171,7 +1009,7 @@ private int matchTokenAt_11(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Description); startRule(context, RuleType.ExamplesDefinition); @@ -1182,7 +1020,7 @@ private int matchTokenAt_11(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Scenario); @@ -1239,7 +1077,7 @@ private int matchTokenAt_11(Token token, ParserContext context) { } final String stateComment = "State: 11 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1280,7 +1118,7 @@ private int matchTokenAt_12(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Step); startRule(context, RuleType.ExamplesDefinition); @@ -1291,7 +1129,7 @@ private int matchTokenAt_12(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Step); endRule(context, RuleType.Scenario); @@ -1353,7 +1191,7 @@ private int matchTokenAt_12(Token token, ParserContext context) { } final String stateComment = "State: 12 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:2>Step:0>#StepLine:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1389,7 +1227,7 @@ private int matchTokenAt_13(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -1401,7 +1239,7 @@ private int matchTokenAt_13(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -1468,7 +1306,7 @@ private int matchTokenAt_13(Token token, ParserContext context) { } final String stateComment = "State: 13 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:0>DataTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1503,7 +1341,7 @@ private int matchTokenAt_14(Token token, ParserContext context) { } final String stateComment = "State: 14 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:0>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#ExamplesLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#ExamplesLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1543,7 +1381,7 @@ private int matchTokenAt_15(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Examples); endRule(context, RuleType.ExamplesDefinition); @@ -1555,7 +1393,7 @@ private int matchTokenAt_15(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Examples); endRule(context, RuleType.ExamplesDefinition); @@ -1618,7 +1456,7 @@ private int matchTokenAt_15(Token token, ParserContext context) { } final String stateComment = "State: 15 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:0>#ExamplesLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1654,7 +1492,7 @@ private int matchTokenAt_16(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Examples); @@ -1667,7 +1505,7 @@ private int matchTokenAt_16(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Examples); @@ -1734,7 +1572,7 @@ private int matchTokenAt_16(Token token, ParserContext context) { } final String stateComment = "State: 16 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1763,7 +1601,7 @@ private int matchTokenAt_17(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.ExamplesTable); endRule(context, RuleType.Examples); @@ -1776,7 +1614,7 @@ private int matchTokenAt_17(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.ExamplesTable); endRule(context, RuleType.Examples); @@ -1848,7 +1686,7 @@ private int matchTokenAt_17(Token token, ParserContext context) { } final String stateComment = "State: 17 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:2>ExamplesTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1882,7 +1720,7 @@ private int matchTokenAt_18(Token token, ParserContext context) { } final String stateComment = "State: 18 - GherkinDocument:0>Feature:3>Rule:0>RuleHeader:0>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -1921,7 +1759,7 @@ private int matchTokenAt_19(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.RuleHeader); startRule(context, RuleType.ScenarioDefinition); @@ -1965,7 +1803,7 @@ private int matchTokenAt_19(Token token, ParserContext context) { } final String stateComment = "State: 19 - GherkinDocument:0>Feature:3>Rule:0>RuleHeader:1>#RuleLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2000,7 +1838,7 @@ private int matchTokenAt_20(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.RuleHeader); @@ -2047,7 +1885,7 @@ private int matchTokenAt_20(Token token, ParserContext context) { } final String stateComment = "State: 20 - GherkinDocument:0>Feature:3>Rule:0>RuleHeader:2>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#BackgroundLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2085,7 +1923,7 @@ private int matchTokenAt_21(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Background); startRule(context, RuleType.ScenarioDefinition); @@ -2129,7 +1967,7 @@ private int matchTokenAt_21(Token token, ParserContext context) { } final String stateComment = "State: 21 - GherkinDocument:0>Feature:3>Rule:1>Background:0>#BackgroundLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2163,7 +2001,7 @@ private int matchTokenAt_22(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Background); @@ -2210,7 +2048,7 @@ private int matchTokenAt_22(Token token, ParserContext context) { } final String stateComment = "State: 22 - GherkinDocument:0>Feature:3>Rule:1>Background:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2251,7 +2089,7 @@ private int matchTokenAt_23(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Step); endRule(context, RuleType.Background); @@ -2303,7 +2141,7 @@ private int matchTokenAt_23(Token token, ParserContext context) { } final String stateComment = "State: 23 - GherkinDocument:0>Feature:3>Rule:1>Background:2>Step:0>#StepLine:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2339,7 +2177,7 @@ private int matchTokenAt_24(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -2395,7 +2233,7 @@ private int matchTokenAt_24(Token token, ParserContext context) { } final String stateComment = "State: 24 - GherkinDocument:0>Feature:3>Rule:1>Background:2>Step:1>StepArg:0>__alt0:0>DataTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2430,7 +2268,7 @@ private int matchTokenAt_25(Token token, ParserContext context) { } final String stateComment = "State: 25 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:0>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#ScenarioLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#ScenarioLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2469,7 +2307,7 @@ private int matchTokenAt_26(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { startRule(context, RuleType.ExamplesDefinition); startRule(context, RuleType.Tags); @@ -2479,7 +2317,7 @@ private int matchTokenAt_26(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Scenario); endRule(context, RuleType.ScenarioDefinition); @@ -2534,7 +2372,7 @@ private int matchTokenAt_26(Token token, ParserContext context) { } final String stateComment = "State: 26 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:0>#ScenarioLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2569,7 +2407,7 @@ private int matchTokenAt_27(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Description); startRule(context, RuleType.ExamplesDefinition); @@ -2580,7 +2418,7 @@ private int matchTokenAt_27(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Scenario); @@ -2639,7 +2477,7 @@ private int matchTokenAt_27(Token token, ParserContext context) { } final String stateComment = "State: 27 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2681,7 +2519,7 @@ private int matchTokenAt_28(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Step); startRule(context, RuleType.ExamplesDefinition); @@ -2692,7 +2530,7 @@ private int matchTokenAt_28(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Step); endRule(context, RuleType.Scenario); @@ -2756,7 +2594,7 @@ private int matchTokenAt_28(Token token, ParserContext context) { } final String stateComment = "State: 28 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:2>Step:0>#StepLine:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#DocStringSeparator", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2793,7 +2631,7 @@ private int matchTokenAt_29(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -2805,7 +2643,7 @@ private int matchTokenAt_29(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DataTable); endRule(context, RuleType.Step); @@ -2874,7 +2712,7 @@ private int matchTokenAt_29(Token token, ParserContext context) { } final String stateComment = "State: 29 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:0>DataTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2909,7 +2747,7 @@ private int matchTokenAt_30(Token token, ParserContext context) { } final String stateComment = "State: 30 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:0>Tags:0>#TagLine:0"; - List expectedTokens = asList("#TagLine", "#ExamplesLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#TagLine", "#ExamplesLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -2950,7 +2788,7 @@ private int matchTokenAt_31(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Examples); endRule(context, RuleType.ExamplesDefinition); @@ -2962,7 +2800,7 @@ private int matchTokenAt_31(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Examples); endRule(context, RuleType.ExamplesDefinition); @@ -3027,7 +2865,7 @@ private int matchTokenAt_31(Token token, ParserContext context) { } final String stateComment = "State: 31 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:0>#ExamplesLine:0"; - List expectedTokens = asList("#EOF", "#Empty", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Empty", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3064,7 +2902,7 @@ private int matchTokenAt_32(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Examples); @@ -3077,7 +2915,7 @@ private int matchTokenAt_32(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.Description); endRule(context, RuleType.Examples); @@ -3146,7 +2984,7 @@ private int matchTokenAt_32(Token token, ParserContext context) { } final String stateComment = "State: 32 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:1>DescriptionHelper:1>Description:0>__alt1:0>#Other:0"; - List expectedTokens = asList("#EOF", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); + List expectedTokens = List.of("#EOF", "#Comment", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3176,7 +3014,7 @@ private int matchTokenAt_33(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.ExamplesTable); endRule(context, RuleType.Examples); @@ -3189,7 +3027,7 @@ private int matchTokenAt_33(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.ExamplesTable); endRule(context, RuleType.Examples); @@ -3263,7 +3101,7 @@ private int matchTokenAt_33(Token token, ParserContext context) { } final String stateComment = "State: 33 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:3>ExamplesDefinition:1>Examples:2>ExamplesTable:0>#TableRow:0"; - List expectedTokens = asList("#EOF", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#TableRow", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3286,7 +3124,7 @@ private int matchTokenAt_35(Token token, ParserContext context) { } final String stateComment = "State: 35 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:1>DocString:0>#DocStringSeparator:0"; - List expectedTokens = asList("#DocStringSeparator", "#Other"); + List expectedTokens = List.of("#DocStringSeparator", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3318,7 +3156,7 @@ private int matchTokenAt_36(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3330,7 +3168,7 @@ private int matchTokenAt_36(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3399,7 +3237,7 @@ private int matchTokenAt_36(Token token, ParserContext context) { } final String stateComment = "State: 36 - GherkinDocument:0>Feature:3>Rule:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:1>DocString:2>#DocStringSeparator:0"; - List expectedTokens = asList("#EOF", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3422,7 +3260,7 @@ private int matchTokenAt_37(Token token, ParserContext context) { } final String stateComment = "State: 37 - GherkinDocument:0>Feature:3>Rule:1>Background:2>Step:1>StepArg:0>__alt0:1>DocString:0>#DocStringSeparator:0"; - List expectedTokens = asList("#DocStringSeparator", "#Other"); + List expectedTokens = List.of("#DocStringSeparator", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3453,7 +3291,7 @@ private int matchTokenAt_38(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3509,7 +3347,7 @@ private int matchTokenAt_38(Token token, ParserContext context) { } final String stateComment = "State: 38 - GherkinDocument:0>Feature:3>Rule:1>Background:2>Step:1>StepArg:0>__alt0:1>DocString:2>#DocStringSeparator:0"; - List expectedTokens = asList("#EOF", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3532,7 +3370,7 @@ private int matchTokenAt_39(Token token, ParserContext context) { } final String stateComment = "State: 39 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:1>DocString:0>#DocStringSeparator:0"; - List expectedTokens = asList("#DocStringSeparator", "#Other"); + List expectedTokens = List.of("#DocStringSeparator", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3563,7 +3401,7 @@ private int matchTokenAt_40(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_1(context, token)) + if (lookahead_1(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3575,7 +3413,7 @@ private int matchTokenAt_40(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3642,7 +3480,7 @@ private int matchTokenAt_40(Token token, ParserContext context) { } final String stateComment = "State: 40 - GherkinDocument:0>Feature:2>ScenarioDefinition:1>Scenario:2>Step:1>StepArg:0>__alt0:1>DocString:2>#DocStringSeparator:0"; - List expectedTokens = asList("#EOF", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#StepLine", "#TagLine", "#ExamplesLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3665,7 +3503,7 @@ private int matchTokenAt_41(Token token, ParserContext context) { } final String stateComment = "State: 41 - GherkinDocument:0>Feature:1>Background:2>Step:1>StepArg:0>__alt0:1>DocString:0>#DocStringSeparator:0"; - List expectedTokens = asList("#DocStringSeparator", "#Other"); + List expectedTokens = List.of("#DocStringSeparator", "#Other"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3695,7 +3533,7 @@ private int matchTokenAt_42(Token token, ParserContext context) { } if (match_TagLine(context, token)) { - if (lookahead_0(context, token)) + if (lookahead_0(context)) { endRule(context, RuleType.DocString); endRule(context, RuleType.Step); @@ -3749,7 +3587,7 @@ private int matchTokenAt_42(Token token, ParserContext context) { } final String stateComment = "State: 42 - GherkinDocument:0>Feature:1>Background:2>Step:1>StepArg:0>__alt0:1>DocString:2>#DocStringSeparator:0"; - List expectedTokens = asList("#EOF", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); + List expectedTokens = List.of("#EOF", "#StepLine", "#TagLine", "#ScenarioLine", "#RuleLine", "#Comment", "#Empty"); ParserException error = token.isEOF() ? new ParserException.UnexpectedEOFException(token, expectedTokens, stateComment) : new ParserException.UnexpectedTokenException(token, expectedTokens, stateComment); @@ -3759,7 +3597,7 @@ private int matchTokenAt_42(Token token, ParserContext context) { } - private boolean lookahead_0(ParserContext context, Token currentToken) { + private boolean lookahead_0(ParserContext context) { Token token; Queue queue = new ArrayDeque(); boolean match = false; @@ -3786,7 +3624,7 @@ private boolean lookahead_0(ParserContext context, Token currentToken) { return match; } - private boolean lookahead_1(ParserContext context, Token currentToken) { + private boolean lookahead_1(ParserContext context) { Token token; Queue queue = new ArrayDeque(); boolean match = false; @@ -3815,27 +3653,152 @@ private boolean lookahead_1(ParserContext context, Token currentToken) { interface Builder { void build(Token token); + void startRule(RuleType ruleType); + void endRule(RuleType ruleType); + T getResult(); + void reset(String uri); } interface TokenMatcher { boolean match_EOF(Token token); + boolean match_Empty(Token token); + boolean match_Comment(Token token); + boolean match_TagLine(Token token); + boolean match_FeatureLine(Token token); + boolean match_RuleLine(Token token); + boolean match_BackgroundLine(Token token); + boolean match_ScenarioLine(Token token); + boolean match_ExamplesLine(Token token); + boolean match_StepLine(Token token); + boolean match_DocStringSeparator(Token token); + boolean match_TableRow(Token token); + boolean match_Language(Token token); + boolean match_Other(Token token); + void reset(); } + + static final class ParserContext { + final TokenScanner tokenScanner; + final TokenMatcher tokenMatcher; + final Queue tokenQueue; + final List errors; + + ParserContext(TokenScanner tokenScanner, TokenMatcher tokenMatcher, Queue tokenQueue, List errors) { + this.tokenScanner = tokenScanner; + this.tokenMatcher = tokenMatcher; + this.tokenQueue = tokenQueue; + this.errors = errors; + } + } + + enum TokenType { + None(RuleType.None), + EOF(RuleType._EOF), + Empty(RuleType._Empty), + Comment(RuleType._Comment), + TagLine(RuleType._TagLine), + FeatureLine(RuleType._FeatureLine), + RuleLine(RuleType._RuleLine), + BackgroundLine(RuleType._BackgroundLine), + ScenarioLine(RuleType._ScenarioLine), + ExamplesLine(RuleType._ExamplesLine), + StepLine(RuleType._StepLine), + DocStringSeparator(RuleType._DocStringSeparator), + TableRow(RuleType._TableRow), + Language(RuleType._Language), + Other(RuleType._Other), + ; + + final RuleType ruleType; + + TokenType(RuleType ruleType) { + this.ruleType = ruleType; + } + } + + enum RuleType { + None, + /** #EOF **/ + _EOF, + /** #Empty **/ + _Empty, + /** #Comment **/ + _Comment, + /** #TagLine **/ + _TagLine, + /** #FeatureLine **/ + _FeatureLine, + /** #RuleLine **/ + _RuleLine, + /** #BackgroundLine **/ + _BackgroundLine, + /** #ScenarioLine **/ + _ScenarioLine, + /** #ExamplesLine **/ + _ExamplesLine, + /** #StepLine **/ + _StepLine, + /** #DocStringSeparator **/ + _DocStringSeparator, + /** #TableRow **/ + _TableRow, + /** #Language **/ + _Language, + /** #Other **/ + _Other, + /** GherkinDocument! := Feature? **/ + GherkinDocument, + /** Feature! := FeatureHeader Background? ScenarioDefinition* Rule* **/ + Feature, + /** FeatureHeader! := #Language? Tags? #FeatureLine DescriptionHelper **/ + FeatureHeader, + /** Rule! := RuleHeader Background? ScenarioDefinition* **/ + Rule, + /** RuleHeader! := Tags? #RuleLine DescriptionHelper **/ + RuleHeader, + /** Background! := #BackgroundLine DescriptionHelper Step* **/ + Background, + /** ScenarioDefinition! [#Empty|#Comment|#TagLine->#ScenarioLine] := Tags? Scenario **/ + ScenarioDefinition, + /** Scenario! := #ScenarioLine DescriptionHelper Step* ExamplesDefinition* **/ + Scenario, + /** ExamplesDefinition! [#Empty|#Comment|#TagLine->#ExamplesLine] := Tags? Examples **/ + ExamplesDefinition, + /** Examples! := #ExamplesLine DescriptionHelper ExamplesTable? **/ + Examples, + /** ExamplesTable! := #TableRow #TableRow* **/ + ExamplesTable, + /** Step! := #StepLine StepArg? **/ + Step, + /** StepArg := (DataTable | DocString) **/ + StepArg, + /** DataTable! := #TableRow+ **/ + DataTable, + /** DocString! := #DocStringSeparator #Other* #DocStringSeparator **/ + DocString, + /** Tags! := #TagLine+ **/ + Tags, + /** DescriptionHelper := #Empty* Description? **/ + DescriptionHelper, + /** Description! := (#Other | #Comment)+ **/ + Description, + } } diff --git a/java/src/main/java/io/cucumber/gherkin/ParserException.java b/java/src/main/java/io/cucumber/gherkin/ParserException.java index 21322154e..a4366d513 100644 --- a/java/src/main/java/io/cucumber/gherkin/ParserException.java +++ b/java/src/main/java/io/cucumber/gherkin/ParserException.java @@ -1,33 +1,34 @@ package io.cucumber.gherkin; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import io.cucumber.messages.types.Location; +import org.jspecify.annotations.Nullable; import static io.cucumber.gherkin.Locations.COLUMN_OFFSET; import static io.cucumber.gherkin.Locations.atColumn; +import static java.util.Objects.requireNonNull; class ParserException extends RuntimeException { - final Location location; + final @Nullable Location location; protected ParserException(String message) { super(message); location = null; } - protected ParserException(String message, Location location) { + protected ParserException(String message, @Nullable Location location) { super(createMessage(message, location)); this.location = location; } - private static String createMessage(String message, Location location) { + private static String createMessage(String message, @Nullable Location location) { if (location == null) { return String.format("(-1,0): %s", message); } - Long line = location.getLine(); - Long column = location.getColumn().orElse(0L); + Integer line = location.getLine(); + Integer column = location.getColumn().orElse(0); return String.format("(%s:%s): %s", line, column, message); } @@ -38,16 +39,16 @@ static final class AstBuilderException extends ParserException { } static final class NoSuchLanguageException extends ParserException { - NoSuchLanguageException(String language, Location location) { + NoSuchLanguageException(String language, @Nullable Location location) { super("Language not supported: " + language, location); } } static final class UnexpectedTokenException extends ParserException { - String stateComment; final Token receivedToken; final List expectedTokenTypes; + final String stateComment; UnexpectedTokenException(Token receivedToken, List expectedTokenTypes, String stateComment) { super(getMessage(receivedToken, expectedTokenTypes), getLocation(receivedToken)); @@ -67,7 +68,7 @@ private static Location getLocation(Token receivedToken) { if (receivedToken.location.getColumn().isPresent()) { return receivedToken.location; } - int column = COLUMN_OFFSET + receivedToken.line.getIndent(); + int column = COLUMN_OFFSET + requireNonNull(receivedToken.line).getIndent(); return atColumn(receivedToken.location, column); } } @@ -93,11 +94,10 @@ static final class CompositeParserException extends ParserException { CompositeParserException(List errors) { super(getMessage(errors)); - this.errors = Collections.unmodifiableList(errors); + this.errors = List.copyOf(errors); } private static String getMessage(List errors) { - if (errors == null) throw new NullPointerException("errors"); return "Parser errors:\n" + errors.stream() .map(Throwable::getMessage) .collect(Collectors.joining("\n")); diff --git a/java/src/main/java/io/cucumber/gherkin/PickleCompiler.java b/java/src/main/java/io/cucumber/gherkin/PickleCompiler.java index bd57cdd7a..9fe09cd24 100644 --- a/java/src/main/java/io/cucumber/gherkin/PickleCompiler.java +++ b/java/src/main/java/io/cucumber/gherkin/PickleCompiler.java @@ -24,15 +24,14 @@ import io.cucumber.messages.types.TableCell; import io.cucumber.messages.types.TableRow; import io.cucumber.messages.types.Tag; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; import static io.cucumber.messages.types.StepKeywordType.CONJUNCTION; import static io.cucumber.messages.types.StepKeywordType.UNKNOWN; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -128,10 +127,10 @@ private List compileTags(List parentTags, List scenarioTags) { return tags; } - private List compilePickleSteps(List backgroundSteps, List scenarioSteps, List variableCells, TableRow valuesRow) { + private List compilePickleSteps(List backgroundSteps, List scenarioSteps, List variableCells, @Nullable TableRow valuesRow) { if (scenarioSteps.isEmpty()) { // usually, a scenario has at least one step, but that's not mandatory - return emptyList(); + return new ArrayList<>(0); } List steps = new ArrayList<>(backgroundSteps.size() + scenarioSteps.size()); StepKeywordType lastKeywordType = UNKNOWN; @@ -160,7 +159,7 @@ private void compileScenarioOutline(List pickles, Scenario scenario, Lis for (final TableRow valuesRow : examples.getTableBody()) { List steps = compilePickleSteps(backgroundSteps, scenario.getSteps(), variableCells, valuesRow); List tags = compileTags(scenarioTags, examples.getTags()); - List sourceIds = asList(scenario.getId(), valuesRow.getId()); + List sourceIds = List.of(scenario.getId(), valuesRow.getId()); Pickle pickle = new Pickle( idGenerator.newId(), uri, @@ -176,12 +175,13 @@ private void compileScenarioOutline(List pickles, Scenario scenario, Lis } } } - - @SuppressWarnings("ForLoopReplaceableByForEach") // classic 'for' loop is ~2x faster than 'for-each' + + @SuppressWarnings("ForLoopReplaceableByForEach") private PickleTable pickleDataTable(DataTable dataTable, List variableCells, List valueCells) { List rows = dataTable.getRows(); int rowCount = rows.size(); List newRows = new ArrayList<>(rowCount); + // classic 'for' loop is ~2x faster than 'for-each' for (int i = 0; i < rowCount; i++) { TableRow row = rows.get(i); List cells = row.getCells(); @@ -203,7 +203,7 @@ private PickleDocString pickleDocString(DocString docString, List var ); } - private PickleStep pickleStep(Step step, List variableCells, TableRow valuesRow, StepKeywordType keywordType) { + private PickleStep pickleStep(Step step, List variableCells, @Nullable TableRow valuesRow, StepKeywordType keywordType) { List valueCells = valuesRow == null ? emptyList() : valuesRow.getCells(); String stepText = interpolate(step.getText(), variableCells, valueCells); @@ -219,10 +219,10 @@ private PickleStep pickleStep(Step step, List variableCells, TableRow List astNodeIds; if (valuesRow != null) { - astNodeIds = Arrays.asList(step.getId(), valuesRow.getId()); + astNodeIds = List.of(step.getId(), valuesRow.getId()); } else { - astNodeIds = singletonList(step.getId()); + astNodeIds = List.of(step.getId()); } return new PickleStep( @@ -235,16 +235,12 @@ private PickleStep pickleStep(Step step, List variableCells, TableRow } private static PickleStepType pickleStepType(StepKeywordType keywordType) { - switch (keywordType) { - case CONTEXT: - return PickleStepType.CONTEXT; - case ACTION: - return PickleStepType.ACTION; - case OUTCOME: - return PickleStepType.OUTCOME; - default: - return PickleStepType.UNKNOWN; - } + return switch (keywordType) { + case CONTEXT -> PickleStepType.CONTEXT; + case ACTION -> PickleStepType.ACTION; + case OUTCOME -> PickleStepType.OUTCOME; + default -> PickleStepType.UNKNOWN; + }; } private PickleStep pickleBackgroundStep(Step step, StepKeywordType keywordType) { @@ -265,7 +261,7 @@ private String interpolate(String name, List variableCells, List pickleTags(List tags) { if (tags.isEmpty()) { - return emptyList(); + return new ArrayList<>(0); } List result = new ArrayList<>(); for (Tag tag : tags) { diff --git a/java/src/main/java/io/cucumber/gherkin/StringUtils.java b/java/src/main/java/io/cucumber/gherkin/StringUtils.java index 154016421..ff8c8e4ca 100644 --- a/java/src/main/java/io/cucumber/gherkin/StringUtils.java +++ b/java/src/main/java/io/cucumber/gherkin/StringUtils.java @@ -3,6 +3,10 @@ final class StringUtils { + private StringUtils(){ + /* no-op */ + } + /** * An extended definition of Whitespace minus new lines. * @@ -24,6 +28,7 @@ static boolean isWhitespaceExcludingNewLine(char c) { * @param c character to test * @return true iff the {@code c} is whitespace. */ + @SuppressWarnings("UnnecessaryParentheses") static boolean isWhitespace(char c) { // This method is about twice faster than `isWhiteSpaceSlow(c)`. // It has been optimized based on the expected use-case of diff --git a/java/src/main/java/io/cucumber/gherkin/TableRowLine.java b/java/src/main/java/io/cucumber/gherkin/TableRowLine.java index 98ad01900..abc26530e 100644 --- a/java/src/main/java/io/cucumber/gherkin/TableRowLine.java +++ b/java/src/main/java/io/cucumber/gherkin/TableRowLine.java @@ -9,6 +9,10 @@ final class TableRowLine { + private TableRowLine(){ + /* no-op */ + } + static List parse(int indent, String text) { List lineSpans = new ArrayList<>(); StringBuilder cellBuilder = new StringBuilder(); @@ -21,20 +25,11 @@ static List parse(int indent, String text) { int c = iterator.next(); if (escape) { switch (c) { - case 'n': - cellBuilder.append('\n'); - break; - case '\\': - cellBuilder.append('\\'); - break; - case '|': - cellBuilder.append('|'); - break; - default: - // Invalid escape. We'll just ignore it. - cellBuilder.append("\\"); - cellBuilder.appendCodePoint(c); - break; + case 'n' -> cellBuilder.append('\n'); + case '\\' -> cellBuilder.append('\\'); + case '|' -> cellBuilder.append('|'); + // Invalid escape. We'll just ignore it. + default -> cellBuilder.append("\\").appendCodePoint(c); } escape = false; } else { @@ -49,7 +44,8 @@ static List parse(int indent, String text) { int column = indent + cellStart + trimmedCellIndent.getIndent() + COLUMN_OFFSET; lineSpans.add(new LineSpan(column, trimmedCellIndent.getText())); } - cellBuilder.setLength(0);// reuse instance rather than creating a new one is faster + // reuse instance rather than creating a new one is faster + cellBuilder.setLength(0); cellStart = col + 1; } else { cellBuilder.appendCodePoint(c); diff --git a/java/src/main/java/io/cucumber/gherkin/TagLine.java b/java/src/main/java/io/cucumber/gherkin/TagLine.java index c8e445bf0..27d6b3482 100644 --- a/java/src/main/java/io/cucumber/gherkin/TagLine.java +++ b/java/src/main/java/io/cucumber/gherkin/TagLine.java @@ -13,6 +13,10 @@ final class TagLine { + private TagLine() { + + } + static List parse(int indent, String text, Location location) { int textLength = text.length(); // parseTags is guarded by token.line.startsWith(TAG_PREFIX_CHAR) @@ -44,9 +48,9 @@ static List parse(int indent, String text, Location location) { if (indexEndCurrentTag > indexStartCurrentTag + 1) { // non-empty tag found, check that the tag does not contain whitespace characters - + // totalCodePointCount is updated incrementally - totalCodePointCount += text.codePointCount(indexStartPreviousTag, indexStartCurrentTag); + totalCodePointCount += text.codePointCount(indexStartPreviousTag, indexStartCurrentTag); indexStartPreviousTag = indexStartCurrentTag; int column = indent + totalCodePointCount + COLUMN_OFFSET; if (containsWhitespace(text, indexStartCurrentTag + 1, indexEndCurrentTag)) { diff --git a/java/src/main/java/io/cucumber/gherkin/Token.java b/java/src/main/java/io/cucumber/gherkin/Token.java index a01c6414a..0cce9f017 100644 --- a/java/src/main/java/io/cucumber/gherkin/Token.java +++ b/java/src/main/java/io/cucumber/gherkin/Token.java @@ -2,24 +2,25 @@ import io.cucumber.messages.types.Location; import io.cucumber.messages.types.StepKeywordType; +import org.jspecify.annotations.Nullable; import java.util.List; import static java.util.Objects.requireNonNull; final class Token { - final Line line; + final @Nullable Line line; final boolean eof; - Parser.TokenType matchedType; - String matchedKeyword; - String matchedText; - List matchedItems; + Parser.@Nullable TokenType matchedType; + @Nullable String matchedKeyword; + @Nullable String matchedText; + @Nullable List matchedItems; int matchedIndent; - String matchedLanguage; - StepKeywordType keywordType; + @Nullable String matchedLanguage; + @Nullable StepKeywordType keywordType; Location location; - private Token(Line line, Location location) { + private Token(@Nullable Line line, Location location) { this.line = line; this.location = location; this.eof = line == null; @@ -41,11 +42,31 @@ boolean isEOF() { } String getTokenValue() { - return isEOF() ? "EOF" : line.getText(); + return line == null ? "EOF" : line.getText(); } @Override public String toString() { return String.format("%s: %s/%s", matchedType, matchedKeyword, matchedText); } + + String getRequiredMatchedKeyword() { + return requireNonNull(matchedKeyword); + } + + String getRequiredMatchedText() { + return requireNonNull(matchedText); + } + + String getRequiredMatchedLanguage() { + return requireNonNull(matchedLanguage); + } + + Parser.TokenType getRequiredMatchedType() { + return requireNonNull(matchedType); + } + + Line getRequiredLine() { + return requireNonNull(line); + } } diff --git a/java/src/main/java/io/cucumber/gherkin/TokenScanner.java b/java/src/main/java/io/cucumber/gherkin/TokenScanner.java index d4c2a774d..f8e02ebce 100644 --- a/java/src/main/java/io/cucumber/gherkin/TokenScanner.java +++ b/java/src/main/java/io/cucumber/gherkin/TokenScanner.java @@ -2,9 +2,7 @@ import io.cucumber.messages.types.Location; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; +import java.util.Iterator; import static io.cucumber.gherkin.Token.createEOF; import static io.cucumber.gherkin.Token.createGherkinLine; @@ -12,30 +10,25 @@ /** * The scanner reads a gherkin doc (typically read from a .feature file) and * creates a token for each line. The tokens are passed to the parser, which - * outputs an AST (Abstract Syntax Tree).

+ * outputs an AST (Abstract Syntax Tree). */ final class TokenScanner { - private final BufferedReader reader; + private final Iterator reader; private int lineNumber; TokenScanner(String source) { - // TODO performance: replace BufferedReader by Java 11 String.lines(source).iterator() : 25-50% faster - this.reader = new BufferedReader(new StringReader(source)); + this.reader = source.lines().iterator(); } Token read() { - try { - String rawText = reader.readLine(); - if (rawText == null) { - // Don't optimistically increment the line number - Location location = Locations.atLine(++lineNumber); - return createEOF(location); - } + if (reader.hasNext()) { + String rawText = reader.next(); Location location = Locations.atLine(++lineNumber); return createGherkinLine(rawText, location); - } catch (IOException e) { - throw new RuntimeException(e); } + // Don't optimistically increment the line number + Location location = Locations.atLine(++lineNumber); + return createEOF(location); } } diff --git a/java/src/main/java/io/cucumber/gherkin/package-info.java b/java/src/main/java/io/cucumber/gherkin/package-info.java new file mode 100644 index 000000000..d26b85f8d --- /dev/null +++ b/java/src/main/java/io/cucumber/gherkin/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.cucumber.gherkin; + +import org.jspecify.annotations.NullMarked; diff --git a/java/src/test/java/io/cucumber/gherkin/AstNodeTest.java b/java/src/test/java/io/cucumber/gherkin/AstNodeTest.java index 20f628a32..3afab31e7 100644 --- a/java/src/test/java/io/cucumber/gherkin/AstNodeTest.java +++ b/java/src/test/java/io/cucumber/gherkin/AstNodeTest.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class AstNodeTest { diff --git a/java/src/test/java/io/cucumber/gherkin/EncodingParserTest.java b/java/src/test/java/io/cucumber/gherkin/EncodingParserTest.java index 62b34208c..744dab791 100644 --- a/java/src/test/java/io/cucumber/gherkin/EncodingParserTest.java +++ b/java/src/test/java/io/cucumber/gherkin/EncodingParserTest.java @@ -15,12 +15,14 @@ void empty() throws RuntimeException { String parsed = readWithEncodingFromSource(utf_8(feature)); assertEquals(feature, parsed); } + @Test void nearly_empty() throws RuntimeException { String feature = "\n"; String parsed = readWithEncodingFromSource(utf_8(feature)); assertEquals(feature, parsed); } + @Test void only_comment() throws RuntimeException { String feature = "#Sample comment\n"; @@ -45,6 +47,7 @@ void single_scenario() throws RuntimeException { String parsed = readWithEncodingFromSource(utf_8(feature)); assertEquals(feature, parsed); } + @Test void reads_with_encoding_from_pragma() throws RuntimeException { String feature = "" + diff --git a/java/src/test/java/io/cucumber/gherkin/ExampleTest.java b/java/src/test/java/io/cucumber/gherkin/ExampleTest.java index a662475bb..a20a627f2 100644 --- a/java/src/test/java/io/cucumber/gherkin/ExampleTest.java +++ b/java/src/test/java/io/cucumber/gherkin/ExampleTest.java @@ -10,12 +10,14 @@ class ExampleTest { @Test void test(){ - String feature = "Feature: Feature 2\n" + - " # some comment\n" + - " some description\n" + - "\n" + - " Scenario: Scenario 1\n" + - " * Wait 3s"; + String feature = """ + Feature: Feature 2 + # some comment + some description + + Scenario: Scenario 1 + * Wait 3s + """; GherkinParser parser = GherkinParser.builder().build(); diff --git a/java/src/test/java/io/cucumber/gherkin/GenerateTokens.java b/java/src/test/java/io/cucumber/gherkin/GenerateTokens.java index 95b3948ca..22801a7e9 100644 --- a/java/src/test/java/io/cucumber/gherkin/GenerateTokens.java +++ b/java/src/test/java/io/cucumber/gherkin/GenerateTokens.java @@ -1,21 +1,53 @@ package io.cucumber.gherkin; +import org.jspecify.annotations.NullMarked; + import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.nio.file.Paths; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.nio.file.Files.readAllBytes; +import static java.nio.file.Files.readString; public final class GenerateTokens { + + private GenerateTokens() { + // main class + } + public static void main(String[] args) throws IOException { TokenFormatterBuilder builder = new TokenFormatterBuilder(); Parser parser = new Parser<>(builder); GherkinTokenMatcher matcher = new GherkinTokenMatcher(); - for (String fileName : args) { - byte[] bytes = readAllBytes(Paths.get(fileName)); - String result = parser.parse(new String(bytes, UTF_8), matcher, fileName); - Stdio.out.print(result); - Stdio.out.flush(); // print doesn't autoflush + OutputStream out = new NonClosableOutputStream(System.out); + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, UTF_8))) { + for (String fileName : args) { + String bytes = readString(Paths.get(fileName), UTF_8); + String result = parser.parse(bytes, matcher, fileName); + writer.print(result); + } + } + } + + @NullMarked + private static class NonClosableOutputStream extends OutputStream { + + private final OutputStream delegate; + + NonClosableOutputStream(OutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); } } } diff --git a/java/src/test/java/io/cucumber/gherkin/GherkinDialectTest.java b/java/src/test/java/io/cucumber/gherkin/GherkinDialectTest.java index 65ae76224..4fd9613e9 100644 --- a/java/src/test/java/io/cucumber/gherkin/GherkinDialectTest.java +++ b/java/src/test/java/io/cucumber/gherkin/GherkinDialectTest.java @@ -7,7 +7,9 @@ import java.util.NoSuchElementException; import java.util.Set; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class GherkinDialectTest { @@ -31,12 +33,6 @@ void getDistinctStepKeywordTypes_Given_has_multiple_stepKeywordTypes() { // Then multiple types are possible assertEquals(1, stepKeywordTypes.size()); } - - @Test - void getDistinctStepKeywordTypes_null_throws() { - // When I get the step keyword types - assertThrows(NullPointerException.class, () -> dialect.getStepKeywordTypesSet(null)); - } @Test void getDistinctStepKeywordTypes_unknown_throws() { diff --git a/java/src/test/java/io/cucumber/gherkin/GherkinDocumentBuilderTest.java b/java/src/test/java/io/cucumber/gherkin/GherkinDocumentBuilderTest.java index 45fd49c28..4603c4b59 100644 --- a/java/src/test/java/io/cucumber/gherkin/GherkinDocumentBuilderTest.java +++ b/java/src/test/java/io/cucumber/gherkin/GherkinDocumentBuilderTest.java @@ -7,20 +7,20 @@ import io.cucumber.messages.types.Location; import io.cucumber.messages.types.Pickle; import io.cucumber.messages.types.TableRow; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; class GherkinDocumentBuilderTest { private final IdGenerator idGenerator = new IncrementingIdGenerator(); @Test void is_reusable() { - Parser parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); + var parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); GherkinTokenMatcher matcher = new GherkinTokenMatcher(); GherkinDocument d1 = parser.parse("Feature: 1", matcher, "1.feature"); @@ -32,27 +32,28 @@ void is_reusable() { @Test void parses_rules() { - Parser parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); - String data = "" + - "Feature: Some rules\n" + - "\n" + - " Background:\n" + - " Given fb\n" + - "\n" + - " Rule: A\n" + - " The rule A description\n" + - "\n" + - " Background:\n" + - " Given ab\n" + - "\n" + - " Example: Example A\n" + - " Given a\n" + - "\n" + - " Rule: B\n" + - " The rule B description\n" + - "\n" + - " Example: Example B\n" + - " Given b\n"; + var parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); + String data = """ + Feature: Some rules + + Background: + Given fb + + Rule: A + The rule A description + + Background: + Given ab + + Example: Example A + Given a + + Rule: B + The rule B description + + Example: Example B + Given b + """; GherkinDocument doc = parser.parse(data, "test.feature"); List children = doc.getFeature().get().getChildren(); @@ -71,7 +72,7 @@ void parses_rules() { @Test void parses_just_comments() { - Parser parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); + var parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); GherkinDocument doc = parser.parse("# Just a comment", "test.feature"); List children = doc.getComments(); assertEquals(1, children.size()); @@ -79,12 +80,13 @@ void parses_just_comments() { @Test void sets_empty_table_cells() { - Parser parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); - GherkinDocument doc = parser.parse("" + - "Feature:\n" + - " Scenario:\n" + - " Given a table\n" + - " |a||b|", + var parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); + GherkinDocument doc = parser.parse(""" + Feature: + Scenario: + Given a table + |a||b| + """, "test.feature" ); List children = doc.getFeature().get().getChildren(); @@ -98,23 +100,25 @@ void sets_empty_table_cells() { @Test void table_cells_with_different_size_throws_exception() { // Given - Parser parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); + var parser = new Parser<>(new GherkinDocumentBuilder(idGenerator, "test.feature")); // When - ParserException.CompositeParserException compositeParserException = assertThrows(ParserException.CompositeParserException.class, () -> parser.parse("" + - "Feature:\n" + - " Scenario:\n" + - " Given a table\n" + - " |a|b|\n" + - " |c|d|e|", + ParserException.CompositeParserException compositeParserException = assertThrows(ParserException.CompositeParserException.class, () -> parser.parse(""" + Feature: + Scenario: + Given a table + |a|b| + |c|d|e| + """, "test.feature" )); // Then - assertTrue(compositeParserException.getMessage().contains("inconsistent cell count within the table")); - Location location = compositeParserException.errors.get(0).location; - assertEquals(5, location.getLine()); - assertEquals(7, location.getColumn().get()); + Assertions.assertThat(compositeParserException).hasMessageContaining("inconsistent cell count within the table"); + Assertions.assertThat(compositeParserException.errors) + .singleElement() + .extracting(e -> e.location) + .isEqualTo(new Location(5, 7)); } } diff --git a/java/src/test/java/io/cucumber/gherkin/GherkinParserBenchmarkTest.java b/java/src/test/java/io/cucumber/gherkin/GherkinParserBenchmarkTest.java index 5054d9363..127d1b41a 100644 --- a/java/src/test/java/io/cucumber/gherkin/GherkinParserBenchmarkTest.java +++ b/java/src/test/java/io/cucumber/gherkin/GherkinParserBenchmarkTest.java @@ -13,9 +13,10 @@ import java.nio.file.Paths; import java.util.stream.Stream; -public class GherkinParserBenchmarkTest { - +public final class GherkinParserBenchmarkTest { + @Benchmark + @SuppressWarnings("UnusedReturnValue") public Stream benchmark() throws IOException { GherkinParser gherkinParser = GherkinParser.builder() .idGenerator(new IncrementingIdGenerator()) diff --git a/java/src/test/java/io/cucumber/gherkin/GherkinParserTest.java b/java/src/test/java/io/cucumber/gherkin/GherkinParserTest.java index 929cadd83..2c629f9ef 100644 --- a/java/src/test/java/io/cucumber/gherkin/GherkinParserTest.java +++ b/java/src/test/java/io/cucumber/gherkin/GherkinParserTest.java @@ -19,16 +19,17 @@ import java.util.stream.Stream; import static io.cucumber.messages.types.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN; -import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class GherkinParserTest { - final String feature = "Feature: Minimal\n" + - "\n" + - " Scenario: minimalistic\n" + - " Given the minimalism\n"; + final String feature = """ + Feature: Minimal + + Scenario: minimalistic + Given the minimalism + """; final Envelope envelope = Envelope.of(new Source("minimal.feature", feature, TEXT_X_CUCUMBER_GHERKIN_PLAIN)); @Test @@ -45,7 +46,7 @@ void can_parse_streams() throws IOException { .includeSource(false) .includeGherkinDocument(false) .build(); - List pickles = parser.parse("minimal.feature", is).collect(toList()); + List pickles = parser.parse("minimal.feature", is).toList(); assertTrue(pickles.get(0).getPickle().isPresent()); } @@ -102,13 +103,15 @@ void parses_supplied_source() { @Test void parser_always_includes_errors() { Envelope singleParseError = Envelope.of(new Source("single_parser_error.feature", - "\n" + - "invalid line here\n" + - "\n" + - "Feature: Single parser error\n" + - "\n" + - " Scenario: minimalistic\n" + - " Given the minimalism\n", + """ + + invalid line here + + Feature: Single parser error + + Scenario: minimalistic + Given the minimalism + """, TEXT_X_CUCUMBER_GHERKIN_PLAIN)); Optional parseError = GherkinParser.builder() .includeSource(false) diff --git a/java/src/test/java/io/cucumber/gherkin/InputStreamsTest.java b/java/src/test/java/io/cucumber/gherkin/InputStreamsTest.java index 386e8ad3e..9a031566f 100644 --- a/java/src/test/java/io/cucumber/gherkin/InputStreamsTest.java +++ b/java/src/test/java/io/cucumber/gherkin/InputStreamsTest.java @@ -15,7 +15,7 @@ class InputStreamsTest { void readsAllBytes() throws IOException { byte[] input = "# sample comment".getBytes(StandardCharsets.UTF_8); try (InputStream is = new ByteArrayInputStream(input)) { - assertArrayEquals(input, InputStreams.readAllBytes(is)); + assertArrayEquals(input, is.readAllBytes()); } } @@ -27,7 +27,7 @@ void readsALotOfBytes() throws IOException { } byte[] input = builder.toString().getBytes(StandardCharsets.UTF_8); try (InputStream is = new ByteArrayInputStream(input)) { - assertArrayEquals(input, InputStreams.readAllBytes(is)); + assertArrayEquals(input, is.readAllBytes()); } } diff --git a/java/src/test/java/io/cucumber/gherkin/IsWhiteSpaceBenchmarkTest.java b/java/src/test/java/io/cucumber/gherkin/IsWhiteSpaceBenchmarkTest.java index 15666778e..b6af1a90d 100644 --- a/java/src/test/java/io/cucumber/gherkin/IsWhiteSpaceBenchmarkTest.java +++ b/java/src/test/java/io/cucumber/gherkin/IsWhiteSpaceBenchmarkTest.java @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class IsWhiteSpaceBenchmarkTest { +public final class IsWhiteSpaceBenchmarkTest { private static final char[] featureFileLinePrefixes = createLinePrefixes(); @@ -48,6 +48,7 @@ private static char[] createLinePrefixes() { } @Benchmark + @SuppressWarnings("UnusedReturnValue") public boolean benchmarkIsWhiteSpace() { boolean hasWhitespace = false; for (char c : featureFileLinePrefixes) { @@ -57,6 +58,7 @@ public boolean benchmarkIsWhiteSpace() { } @Benchmark + @SuppressWarnings("UnusedReturnValue") public boolean benchmarkIsWhiteSpaceSlow() { boolean hasWhitespace = false; for (char c : featureFileLinePrefixes) { diff --git a/java/src/test/java/io/cucumber/gherkin/KeywordMatchersTest.java b/java/src/test/java/io/cucumber/gherkin/KeywordMatchersTest.java index 1548d2308..18d6fadad 100644 --- a/java/src/test/java/io/cucumber/gherkin/KeywordMatchersTest.java +++ b/java/src/test/java/io/cucumber/gherkin/KeywordMatchersTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.util.Collection; +import java.util.Objects; import java.util.Set; import static io.cucumber.gherkin.Constants.TITLE_KEYWORD_SEPARATOR_LENGTH; @@ -18,18 +19,21 @@ static Collection languages() { return GherkinDialects.getDialects(); } + private static KeywordMatcher requiredMatcher(GherkinDialect dialect) { + return Objects.requireNonNull(KeywordMatchers.of(dialect.getLanguage())); + } + @ParameterizedTest @MethodSource("languages") void featureKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); - assertThat(matcher).isNotNull(); + KeywordMatcher matcher = requiredMatcher(dialect); for (String keyword : dialect.getFeatureKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchFeatureKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } } @@ -37,15 +41,15 @@ void featureKeywordsAreConsistent(GherkinDialect dialect) { @ParameterizedTest @MethodSource("languages") void backgroundKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); + KeywordMatcher matcher = requiredMatcher(dialect); assertThat(matcher).isNotNull(); for (String keyword : dialect.getBackgroundKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchBackgroundKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } } @@ -53,15 +57,15 @@ void backgroundKeywordsAreConsistent(GherkinDialect dialect) { @ParameterizedTest @MethodSource("languages") void ruleKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); + KeywordMatcher matcher = requiredMatcher(dialect); assertThat(matcher).isNotNull(); for (String keyword : dialect.getRuleKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchRuleKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } } @@ -69,23 +73,23 @@ void ruleKeywordsAreConsistent(GherkinDialect dialect) { @ParameterizedTest @MethodSource("languages") void scenarioKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); + KeywordMatcher matcher = requiredMatcher(dialect); assertThat(matcher).isNotNull(); for (String keyword : dialect.getScenarioKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchScenarioKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } for (String keyword : dialect.getScenarioOutlineKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchScenarioKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } } @@ -93,15 +97,15 @@ void scenarioKeywordsAreConsistent(GherkinDialect dialect) { @ParameterizedTest @MethodSource("languages") void exampleKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); + KeywordMatcher matcher = requiredMatcher(dialect); assertThat(matcher).isNotNull(); for (String keyword : dialect.getExamplesKeywords()) { Line line = new Line(keyword + ": some text"); KeywordMatcher.Match match = matcher.matchExampleKeyword(line); assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(KeywordMatcher.Match::getKeywordLength).isEqualTo(keyword.length() + TITLE_KEYWORD_SEPARATOR_LENGTH) ); } } @@ -109,21 +113,21 @@ void exampleKeywordsAreConsistent(GherkinDialect dialect) { @ParameterizedTest @MethodSource("languages") void stepKeywordsAreConsistent(GherkinDialect dialect) { - KeywordMatcher matcher = KeywordMatchers.of(dialect.getLanguage()); + KeywordMatcher matcher = requiredMatcher(dialect); assertThat(matcher).isNotNull(); for (String keyword : dialect.getStepKeywords()) { Line line = new Line(keyword + "some text"); StepMatch match = matcher.matchStepKeyword(line); - assertAll( - () -> assertThat(match.getKeyword()).isEqualTo(keyword), - () -> assertThat(match.getKeywordLength()).isEqualTo(keyword.length()), + () -> assertThat(match).extracting(StepMatch::getKeyword).isEqualTo(keyword), + () -> assertThat(match).extracting(StepMatch::getKeywordLength).isEqualTo(keyword.length()), () -> { Set stepKeywordTypesSet = dialect.getStepKeywordTypesSet(keyword); StepKeywordType expected = stepKeywordTypesSet.size() != 1 ? StepKeywordType.UNKNOWN : stepKeywordTypesSet.iterator().next(); - assertThat(match.getKeywordType()).isEqualTo(expected); - }); + assertThat(match).extracting(StepMatch::getKeywordType).isEqualTo(expected); + } + ); } } } diff --git a/java/src/test/java/io/cucumber/gherkin/LineTest.java b/java/src/test/java/io/cucumber/gherkin/LineTest.java index 06b2f9fa9..7da42564d 100644 --- a/java/src/test/java/io/cucumber/gherkin/LineTest.java +++ b/java/src/test/java/io/cucumber/gherkin/LineTest.java @@ -25,9 +25,12 @@ void startsWithTitleKeyword_non_corresponding_keyword_does_not_match() { Line gherkinLine = new Line("Rule: X"); // When/Then - assertFalse(gherkinLine.startsWithTitleKeyword("Background")); // not the same keyword - assertFalse(gherkinLine.startsWithTitleKeyword("Rule: X")); // same keyword but with colon - assertFalse(gherkinLine.startsWithTitleKeyword("Rul")); // shorter than keyword + // not the same keyword + assertFalse(gherkinLine.startsWithTitleKeyword("Background")); + // same keyword but with colon + assertFalse(gherkinLine.startsWithTitleKeyword("Rule: X")); + // shorter than keyword + assertFalse(gherkinLine.startsWithTitleKeyword("Rul")); } @Test diff --git a/java/src/test/java/io/cucumber/gherkin/LocationsTest.java b/java/src/test/java/io/cucumber/gherkin/LocationsTest.java index 0f4d963ca..03b68339d 100644 --- a/java/src/test/java/io/cucumber/gherkin/LocationsTest.java +++ b/java/src/test/java/io/cucumber/gherkin/LocationsTest.java @@ -5,7 +5,9 @@ import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class LocationsTest { @@ -13,10 +15,10 @@ class LocationsTest { void atLine() { // random integer conversion that is far after the cache upper bound works Location location = Locations.atLine(12000); - assertEquals(Long.valueOf(12000L), location.getLine()); + assertEquals(Integer.valueOf(12000), location.getLine()); // sequential integer conversion works (the cache has no hole) for (int i = 0; i < 12000; i++) { - Long expectedLine = (long) i; + Integer expectedLine = i; Location actual = Locations.atLine(i); assertEquals(expectedLine, actual.getLine()); } @@ -37,7 +39,7 @@ void atLine_multithreaded_does_not_raise_exception() { for (int i = 0; i < numberOfThreads; i++) { threads[i] = new Thread(() -> { for (int j = 500; j < numberOfIterations; j *= 2) { - Long expectedLine = (long) j; + Integer expectedLine = j; Location actual = Locations.atLine(j); assertEquals(expectedLine, actual.getLine()); } diff --git a/java/src/test/java/io/cucumber/gherkin/Main.java b/java/src/test/java/io/cucumber/gherkin/Main.java index 5d33c6ff8..1a84e8c4a 100644 --- a/java/src/test/java/io/cucumber/gherkin/Main.java +++ b/java/src/test/java/io/cucumber/gherkin/Main.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -13,8 +14,12 @@ import static io.cucumber.gherkin.Jackson.OBJECT_MAPPER; import static java.util.Arrays.asList; -public class Main { +public final class Main { + private Main(){ + // main class + } + public static void main(String[] argv) throws IOException { List args = new ArrayList<>(asList(argv)); List paths = new ArrayList<>(); @@ -25,26 +30,18 @@ public static void main(String[] argv) throws IOException { String arg = args.remove(0).trim(); switch (arg) { - case "--no-source": - builder.includeSource(false); - break; - case "--no-ast": - builder.includeGherkinDocument(false); - break; - case "--no-pickles": - builder.includePickles(false); - break; - case "--predictable-ids": - builder.idGenerator(new IncrementingIdGenerator()); - break; - default: - paths.add(arg); + case "--no-source" -> builder.includeSource(false); + case "--no-ast" -> builder.includeGherkinDocument(false); + case "--no-pickles" -> builder.includePickles(false); + case "--predictable-ids" -> builder.idGenerator(new IncrementingIdGenerator()); + default -> paths.add(arg); } } GherkinParser parser = builder.build(); - try (MessageToNdjsonWriter writer = new MessageToNdjsonWriter(System.out, OBJECT_MAPPER::writeValue)) { + OutputStream out = new NonClosableOutputStream(System.out); + try (MessageToNdjsonWriter writer = new MessageToNdjsonWriter(out, OBJECT_MAPPER::writeValue)) { for (String path : paths) { try (InputStream fis = Files.newInputStream(Paths.get(path))) { // Don't use parser.parse(Path). The test suite uses relative paths. @@ -63,4 +60,21 @@ private static void printMessage(MessageToNdjsonWriter writer, Envelope envelope } } + private static final class NonClosableOutputStream extends OutputStream { + private final OutputStream delegate; + + private NonClosableOutputStream(OutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + } + } } diff --git a/java/src/test/java/io/cucumber/gherkin/Stdio.java b/java/src/test/java/io/cucumber/gherkin/Stdio.java deleted file mode 100644 index 2f9762639..000000000 --- a/java/src/test/java/io/cucumber/gherkin/Stdio.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.cucumber.gherkin; - -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; - -class Stdio { - /** - * UTF-8 STDOUT - */ - public static final PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true); - -} diff --git a/java/src/test/java/io/cucumber/gherkin/TableRowLineTest.java b/java/src/test/java/io/cucumber/gherkin/TableRowLineTest.java index 7f1aca41b..7b25a6946 100644 --- a/java/src/test/java/io/cucumber/gherkin/TableRowLineTest.java +++ b/java/src/test/java/io/cucumber/gherkin/TableRowLineTest.java @@ -6,11 +6,11 @@ import java.util.stream.Collectors; import static io.cucumber.gherkin.TableRowLine.parse; -import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; class TableRowLineTest { + @SuppressWarnings({"UnnecessaryUnicodeEscape", "UnicodeEscape"}) @Test void finds_table_cells() { // The cells below has the following whitespace characters on each side: @@ -42,18 +42,19 @@ void finds_table_cells() { void finds_escaped_table_cells() { List tableCells = parse(6, "| \\|æ\\\\n | \\o\\no\\ | \\\\\\|a\\\\\\\\n | ø\\\\\\nø\\\\|"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("|æ\\n", "\\o\no\\", "\\|a\\\\n", "ø\\\nø\\"), texts); + assertEquals(List.of("|æ\\n", "\\o\no\\", "\\|a\\\\n", "ø\\\nø\\"), texts); } @Test void preserve_escaped_new_lines_at_start_and_end() { List tableCells = parse(6, "| \nraindrops--\nher last kiss\ngoodbye.\n |"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("" + - "\n" + - "raindrops--\n" + - "her last kiss\n" + - "goodbye.\n" + assertEquals(List.of(""" + + raindrops-- + her last kiss + goodbye. + """ ), texts); } @@ -61,41 +62,41 @@ void preserve_escaped_new_lines_at_start_and_end() { void escapes_backslash() { List tableCells = parse(0, "|\\\\o\\no\\||"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("\\o\no|"), texts); + assertEquals(List.of("\\o\no|"), texts); } @Test void throws_on_illegal_escapes_backslash() { List tableCells = parse(0, "|\\o\\no\\||"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("\\o\no|"), texts); + assertEquals(List.of("\\o\no|"), texts); } @Test void correctly_trims_white_spaces_before_cell_content() { List tableCells = parse(0, "| \t spaces before|"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("spaces before"), texts); + assertEquals(List.of("spaces before"), texts); } @Test void correctly_trims_white_spaces_after_cell_content() { List tableCells = parse(0, "|spaces after |"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("spaces after"), texts); + assertEquals(List.of("spaces after"), texts); } @Test void correctly_trims_white_spaces_around_cell_content() { List tableCells = parse(0, "| \t spaces everywhere \t|"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("spaces everywhere"), texts); + assertEquals(List.of("spaces everywhere"), texts); } @Test void does_not_drop_white_spaces_inside_a_cell() { List tableCells = parse(0, "| foo()\n bar\nbaz |"); List texts = tableCells.stream().map(span -> span.text).collect(Collectors.toList()); - assertEquals(asList("foo()\n bar\nbaz"), texts); + assertEquals(List.of("foo()\n bar\nbaz"), texts); } } diff --git a/java/src/test/java/io/cucumber/gherkin/TestDataTest.java b/java/src/test/java/io/cucumber/gherkin/TestDataTest.java deleted file mode 100644 index e5f0c844b..000000000 --- a/java/src/test/java/io/cucumber/gherkin/TestDataTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.cucumber.gherkin; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; - -public class TestDataTest { - @Test - void testdata_features_are_parsed_without_NPE() throws IOException { - GherkinParser gherkinParser = GherkinParser.builder() - .idGenerator(new IncrementingIdGenerator()) - .build(); - try (Stream list = Stream.of( - Files.list(Paths.get("../testdata/good/")), - Files.list(Paths.get("../testdata/bad/"))) - .flatMap(s -> s)) { - list - .filter(path -> path.toString().endsWith(".feature")) - .forEach(source -> { - try { - gherkinParser.parse(source); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - } -} diff --git a/java/src/test/java/io/cucumber/gherkin/TokenFormatter.java b/java/src/test/java/io/cucumber/gherkin/TokenFormatter.java index 2dc691a61..2257fe0eb 100644 --- a/java/src/test/java/io/cucumber/gherkin/TokenFormatter.java +++ b/java/src/test/java/io/cucumber/gherkin/TokenFormatter.java @@ -1,5 +1,7 @@ package io.cucumber.gherkin; +import org.jspecify.annotations.Nullable; + import static java.util.stream.Collectors.joining; class TokenFormatter { @@ -10,7 +12,7 @@ String formatToken(Token token) { return String.format("(%s:%s)%s:%s/%s/%s", toString(token.location.getLine()), - toString(token.location.getColumn().orElse(0L)), + toString(token.location.getColumn().orElse(0)), toString(token.matchedType), token.matchedKeyword == null ? "" : String.format("(%s)%s", toString(token.keywordType), @@ -22,7 +24,7 @@ String formatToken(Token token) { ); } - private String toString(Object o) { + private String toString(@Nullable Object o) { return o == null ? "" : o.toString(); } }