Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
### Added
- [Java] Add OSGi metadata ([#485](https://github.com/cucumber/gherkin/pull/485))

### Fixed
- [Java] Fixed `AstNode` conditions which never occur in `GherkinDocumentBuilder`.

## [36.0.0] - 2025-10-09
### Changed
- [.NET, Elixir, Go, JavaScript, Java, Perl, Php, Ruby] Update dependency messages to v30
Expand Down
14 changes: 13 additions & 1 deletion java/src/main/java/io/cucumber/gherkin/AstNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ <T> T getSingle(RuleType ruleType, T defaultResult) {
return items == null ? defaultResult : items.get(0);
}

@SuppressWarnings("unchecked")
<T> 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<T> items = (List<T>) subItems.get(ruleType);
return items == null ? null : items.get(0);
}

<T> T getRequiredSingle(RuleType ruleType) {
return requireNonNull(getSingle(ruleType));
}

@SuppressWarnings("unchecked")
<T> List<T> getItems(RuleType ruleType) {
List<T> items = (List<T>) subItems.get(ruleType);
Expand All @@ -48,7 +60,7 @@ <T> List<T> getItems(RuleType ruleType) {
}

Token getToken(TokenType tokenType) {
return requireNonNull(getSingle(tokenType.ruleType, null));
return getRequiredSingle(tokenType.ruleType);
}

List<Token> getTokens(TokenType tokenType) {
Expand Down
47 changes: 27 additions & 20 deletions java/src/main/java/io/cucumber/gherkin/GherkinDocumentBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ private Object getTransformedNode(AstNode node) {
stepLine.matchedKeyword,
stepLine.keywordType,
stepLine.matchedText,
node.getSingle(RuleType.DocString, null),
node.getSingle(RuleType.DataTable, null),
node.getSingle(RuleType.DocString),
node.getSingle(RuleType.DataTable),
idGenerator.newId()
);
}
Expand Down Expand Up @@ -116,7 +116,7 @@ private Object getTransformedNode(AstNode node) {
);
}
case ScenarioDefinition: {
AstNode scenarioNode = node.getSingle(RuleType.Scenario, null);
AstNode scenarioNode = node.getRequiredSingle(RuleType.Scenario);
Token scenarioLine = scenarioNode.getToken(TokenType.ScenarioLine);

return new Scenario(
Expand All @@ -131,11 +131,19 @@ private Object getTransformedNode(AstNode node) {
);
}
case ExamplesDefinition: {
AstNode examplesNode = node.getSingle(RuleType.Examples, null);
AstNode examplesNode = node.getRequiredSingle(RuleType.Examples);
Token examplesLine = examplesNode.getToken(TokenType.ExamplesLine);
List<TableRow> rows = examplesNode.getSingle(RuleType.ExamplesTable, null);
TableRow tableHeader = rows != null && !rows.isEmpty() ? rows.get(0) : null;
List<TableRow> tableBody = rows != null && !rows.isEmpty() ? rows.subList(1, rows.size()) : Collections.emptyList();
// rows is null when a Scenario Outline has no Examples table
List<TableRow> rows = examplesNode.getSingle(RuleType.ExamplesTable);
TableRow tableHeader;
List<TableRow> tableBody;
if (rows != null && !rows.isEmpty()) {
tableHeader = rows.get(0);
tableBody = rows.subList(1, rows.size());
} else {
tableHeader = null;
tableBody = Collections.emptyList();
}

return new Examples(
examplesLine.location,
Expand All @@ -146,7 +154,6 @@ private Object getTransformedNode(AstNode node) {
tableHeader,
tableBody,
idGenerator.newId()

);
}
case ExamplesTable: {
Expand All @@ -162,14 +169,13 @@ private Object getTransformedNode(AstNode node) {
return joinMatchedText(lineTokens, toIndex);
}
case Feature: {
AstNode header = node.getSingle(RuleType.FeatureHeader, new AstNode(RuleType.FeatureHeader));
if (header == null) return null;
AstNode header = node.getRequiredSingle(RuleType.FeatureHeader);
List<Tag> tags = getTags(header);
Token featureLine = header.getToken(TokenType.FeatureLine);
if (featureLine == null) return null;

List<FeatureChild> children = new ArrayList<>();
Background background = node.getSingle(RuleType.Background, null);
// Background is an optional element of a Feature, so can be null
Background background = node.getSingle(RuleType.Background);
if (background != null) {
children.add(new FeatureChild(null, background, null));
}
Expand All @@ -191,15 +197,14 @@ private Object getTransformedNode(AstNode node) {
);
}
case Rule: {
AstNode header = node.getSingle(RuleType.RuleHeader, new AstNode(RuleType.RuleHeader));
if (header == null) return null;
AstNode header = node.getRequiredSingle(RuleType.RuleHeader);
Token ruleLine = header.getToken(TokenType.RuleLine);
if (ruleLine == null) return null;

List<RuleChild> children = new ArrayList<>();
List<Tag> tags = getTags(header);

Background background = node.getSingle(RuleType.Background, null);
// Background is an optional element of a Feature, so can be null
Background background = node.getSingle(RuleType.Background);
if (background != null) {
children.add(new RuleChild(background, null));
}
Expand All @@ -220,7 +225,8 @@ private Object getTransformedNode(AstNode node) {

}
case GherkinDocument: {
Feature feature = node.getSingle(RuleType.Feature, null);
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);
}

Expand Down Expand Up @@ -293,9 +299,10 @@ private String getDescription(AstNode node) {
}

private List<Tag> getTags(AstNode node) {
AstNode tagsNode = node.getSingle(RuleType.Tags, null);
if (tagsNode == null)
AstNode tagsNode = node.getSingle(RuleType.Tags);
if (tagsNode == null) {// tags are optional
return Collections.emptyList();
}

List<Token> tokens = tagsNode.getTokens(TokenType.TagLine);
List<Tag> tags = new ArrayList<>();
Expand All @@ -309,7 +316,7 @@ private List<Tag> getTags(AstNode node) {

@Override
public GherkinDocument getResult() {
return currentNode().getSingle(RuleType.GherkinDocument, null);
return currentNode().getSingle(RuleType.GherkinDocument);
}

}
22 changes: 22 additions & 0 deletions java/src/test/java/io/cucumber/gherkin/AstNodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,26 @@ void getSingle_returns_first_item() {
// Then
assertEquals(item1, astNode.getSingle(Parser.RuleType.Step, "defaultValue"));
}

@Test
void getRequiredSingle_throws_exception_when_no_items() {
// Given a node
AstNode astNode = new AstNode(Parser.RuleType.Step);

// When no subItem is present

// Then
assertThrows(NullPointerException.class, () -> astNode.getRequiredSingle(Parser.RuleType.Scenario));
}

@Test
void getSingle_return_null_when_no_items() {
// Given a node
AstNode astNode = new AstNode(Parser.RuleType.Step);

// When no subItem is present

// Then
assertNull(astNode.getSingle(Parser.RuleType.Scenario));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ void sets_empty_table_cells() {
" |a||b|",
"test.feature"
);
TableRow row = doc.getFeature().get().getChildren().get(0).getScenario().get().getSteps().get(0).getDataTable().get().getRows().get(0);
List<FeatureChild> children = doc.getFeature().get().getChildren();
assertEquals(1, children.size());
TableRow row = children.get(0).getScenario().get().getSteps().get(0).getDataTable().get().getRows().get(0);
assertEquals("a", row.getCells().get(0).getValue());
assertEquals("", row.getCells().get(1).getValue());
assertEquals("b", row.getCells().get(2).getValue());
Expand Down
32 changes: 32 additions & 0 deletions java/src/test/java/io/cucumber/gherkin/TestDataTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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<Path> 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);
}
});
}
}
}