Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[.NET] Avoid allocation and improve parsing time #344

Merged
merged 6 commits into from
Feb 5, 2025
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
- [cpp] namespace was changed to 'cucumber::gherkin' to better reflect project structure and prevent clashing
- [.NET] Removed dependency on System.Text.Json and related logic in GherkinDialectProvider
- [Elixir] Updates dependencies, bumps messages to 27.0.2
- [.NET] Changed some types from class to struct, removed IGherkinLine interface and changes some functions from Array to Enumerable

### Removed
- [Python] Dropped support for Python 3.8
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin.Specs/EventStubs/GherkinEventsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private void AddParseError(List<Envelope> events, ParserException e, String uri)
Message = e.Message,
Source = new SourceReference()
{
Location = new Location(e.Location.Column, e.Location.Line),
Location = e.Location.HasValue ? new Location(e.Location.GetValueOrDefault().Column, e.Location.GetValueOrDefault().Line) : null,
Uri = uri
}
}
Expand Down
12 changes: 7 additions & 5 deletions dotnet/Gherkin.Specs/Tokens/TestTokenFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public string FormatToken(Token token)
if (token.IsEOF)
return "EOF";

string stepTypeText;
string stepTypeText = string.Empty;
string matchedItemsText = null;
switch (token.MatchedType)
{
case TokenType.FeatureLine:
Expand All @@ -22,13 +23,14 @@ public string FormatToken(Token token)
var tokenType = token.MatchedGherkinDialect.GetStepKeywordType(token.MatchedKeyword);
stepTypeText = $"({tokenType})";
break;
default:
stepTypeText = "";
case TokenType.TagLine:
matchedItemsText = string.Join(",", token.Line.GetTags().Select(i => i.Column + ":" + i.Text));
break;
case TokenType.TableRow:
matchedItemsText = string.Join(",", token.Line.GetTableCells().Select(i => i.Column + ":" + i.Text));
break;
}

var matchedItemsText = token.MatchedItems == null ? "" : string.Join(",", token.MatchedItems.Select(i => i.Column + ":" + i.Text));

return $"({token.Location.Line}:{token.Location.Column}){token.MatchedType}:{stepTypeText}{token.MatchedKeyword}/{token.MatchedText}/{matchedItemsText}";
}
}
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Background.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Gherkin.Ast;

public class Background(Location location, string keyword, string name, string description, Step[] steps)
public class Background(Location location, string keyword, string name, string description, IEnumerable<Step> steps)
: StepsContainer(location, keyword, name, description, steps);
4 changes: 2 additions & 2 deletions dotnet/Gherkin/Ast/DataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ public class DataTable : StepArgument, IHasRows, IHasLocation
public Location Location { get; private set; }
public IEnumerable<TableRow> Rows { get; private set; }

public DataTable(TableRow[] rows)
public DataTable(List<TableRow> rows)
{
if (rows == null) throw new ArgumentNullException("rows");
if (rows.Length == 0) throw new ArgumentException("DataTable must have at least one row", "rows");
if (rows.Count == 0) throw new ArgumentException("DataTable must have at least one row", "rows");

Location = rows[0].Location;
Rows = rows;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Examples.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class Examples(Tag[] tags, Location location, string keyword, string name, string description, TableRow header, TableRow[] body)
public class Examples(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, TableRow header, IEnumerable<TableRow> body)
: IHasLocation, IHasDescription, IHasRows, IHasTags
{
public IEnumerable<Tag> Tags { get; } = tags;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Feature.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class Feature(Tag[] tags, Location location, string language, string keyword, string name, string description, IHasLocation[] children)
public class Feature(IEnumerable<Tag> tags, Location location, string language, string keyword, string name, string description, IEnumerable<IHasLocation> children)
: IHasLocation, IHasDescription, IHasTags, IHasChildren
{
public IEnumerable<Tag> Tags { get; } = tags;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/GherkinDocument.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class GherkinDocument(Feature feature, Comment[] comments)
public class GherkinDocument(Feature feature, IEnumerable<Comment> comments)
{
public Feature Feature { get; } = feature;
public IEnumerable<Comment> Comments { get; } = comments;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Location.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class Location(int line = 0, int column = 0)
public readonly struct Location(int line = 0, int column = 0)
{
public int Line { get; } = line;
public int Column { get; } = column;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Rule.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class Rule(Tag[] tags, Location location, string keyword, string name, string description, IHasLocation[] children)
public class Rule(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, IEnumerable<IHasLocation> children)
: IHasLocation, IHasDescription, IHasChildren, IHasTags
{
public Location Location { get; } = location;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/Scenario.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class Scenario(Tag[] tags, Location location, string keyword, string name, string description, Step[] steps, Examples[] examples)
public class Scenario(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, IEnumerable<Step> steps, IEnumerable<Examples> examples)
: StepsContainer(location, keyword, name, description, steps), IHasTags
{
public IEnumerable<Tag> Tags { get; } = tags;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/StepsContainer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public abstract class StepsContainer(Location location, string keyword, string name, string description, Step[] steps)
public abstract class StepsContainer(Location location, string keyword, string name, string description, IEnumerable<Step> steps)
: IHasLocation, IHasDescription, IHasSteps
{
public Location Location { get; } = location;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/TableCell.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class TableCell(Location location, string value) : IHasLocation
public readonly struct TableCell(Location location, string value) : IHasLocation
{
public Location Location { get; } = location;
public string Value { get; } = value;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/Gherkin/Ast/TableRow.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Gherkin.Ast;

public class TableRow(Location location, TableCell[] cells) : IHasLocation
public class TableRow(Location location, IEnumerable<TableCell> cells) : IHasLocation
{
public Location Location { get; } = location;
public IEnumerable<TableCell> Cells { get; } = cells;
Expand Down
109 changes: 51 additions & 58 deletions dotnet/Gherkin/AstBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private object GetTransformedNode(AstNode node)

var description = GetDescription(scenarioNode);
var steps = GetSteps(scenarioNode);
var examples = scenarioNode.GetItems<Examples>(RuleType.ExamplesDefinition).ToArray();
List<Examples> examples = [.. scenarioNode.GetItems<Examples>(RuleType.ExamplesDefinition)];
return CreateScenario(tags, GetLocation(scenarioLine), scenarioLine.MatchedKeyword, scenarioLine.MatchedText, description, steps, examples, node);
}
case RuleType.ExamplesDefinition:
Expand All @@ -102,9 +102,9 @@ private object GetTransformedNode(AstNode node)
var examplesLine = examplesNode.GetToken(TokenType.ExamplesLine);
var description = GetDescription(examplesNode);

var allRows = examplesNode.GetSingle<TableRow[]>(RuleType.ExamplesTable);
var header = allRows != null ? allRows.First() : null;
var rows = allRows != null ? allRows.Skip(1).ToArray() : null;
var allRows = examplesNode.GetSingle<List<TableRow>>(RuleType.ExamplesTable);
var header = allRows != null ? allRows[0] : null;
var rows = allRows != null ? allRows.Skip(1).ToList() : null;
return CreateExamples(tags, GetLocation(examplesLine), examplesLine.MatchedKeyword, examplesLine.MatchedText, description, header, rows, node);
}
case RuleType.ExamplesTable:
Expand All @@ -113,7 +113,7 @@ private object GetTransformedNode(AstNode node)
}
case RuleType.Description:
{
var lineTokens = node.GetTokens(TokenType.Other);
IEnumerable<Token> lineTokens = node.GetTokens(TokenType.Other);

// Trim trailing empty lines
lineTokens = lineTokens.Reverse().SkipWhile(t => string.IsNullOrWhiteSpace(t.MatchedText)).Reverse();
Expand All @@ -130,16 +130,16 @@ private object GetTransformedNode(AstNode node)
var children = new List<IHasLocation>();
var background = node.GetSingle<Background>(RuleType.Background);
if (background != null)
{
children.Add(background);
}
var childrenEnumerable = children.Concat(node.GetItems<IHasLocation>(RuleType.ScenarioDefinition))
.Concat(node.GetItems<IHasLocation>(RuleType.Rule));
foreach (var scenarioDefinition in node.GetItems<IHasLocation>(RuleType.ScenarioDefinition))
children.Add(scenarioDefinition);
foreach (var rule in node.GetItems<IHasLocation>(RuleType.Rule))
children.Add(rule);
var description = GetDescription(header);
if (featureLine.MatchedGherkinDialect == null) return null;
var language = featureLine.MatchedGherkinDialect.Language;

return CreateFeature(tags, GetLocation(featureLine), language, featureLine.MatchedKeyword, featureLine.MatchedText, description, childrenEnumerable.ToArray(), node);
return CreateFeature(tags, GetLocation(featureLine), language, featureLine.MatchedKeyword, featureLine.MatchedText, description, children, node);
}
case RuleType.Rule:
{
Expand All @@ -151,14 +151,13 @@ private object GetTransformedNode(AstNode node)
var children = new List<IHasLocation>();
var background = node.GetSingle<Background>(RuleType.Background);
if (background != null)
{
children.Add(background);
}
var childrenEnumerable = children.Concat(node.GetItems<IHasLocation>(RuleType.ScenarioDefinition));
foreach (var scenarioDefinition in node.GetItems<IHasLocation>(RuleType.ScenarioDefinition))
children.Add(scenarioDefinition);
var description = GetDescription(header);
if (ruleLine.MatchedGherkinDialect == null) return null;

return CreateRule(tags, GetLocation(ruleLine), ruleLine.MatchedKeyword, ruleLine.MatchedText, description, childrenEnumerable.ToArray(), node);
return CreateRule(tags, GetLocation(ruleLine), ruleLine.MatchedKeyword, ruleLine.MatchedText, description, children, node);
}
case RuleType.GherkinDocument:
{
Expand All @@ -179,12 +178,12 @@ protected virtual StepKeywordType GetKeywordType(Token stepLine)
return stepKeywordType.Value;
}

protected virtual Background CreateBackground(Location location, string keyword, string name, string description, Step[] steps, AstNode node)
protected virtual Background CreateBackground(Location location, string keyword, string name, string description, IEnumerable<Step> steps, AstNode node)
{
return new Background(location, keyword, name, description, steps);
}

protected virtual DataTable CreateDataTable(TableRow[] rows, AstNode node)
protected virtual DataTable CreateDataTable(List<TableRow> rows, AstNode node)
{
return new DataTable(rows);
}
Expand All @@ -194,12 +193,12 @@ protected virtual Comment CreateComment(Location location, string text)
return new Comment(location, text);
}

protected virtual Examples CreateExamples(Tag[] tags, Location location, string keyword, string name, string description, TableRow header, TableRow[] body, AstNode node)
protected virtual Examples CreateExamples(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, TableRow header, IEnumerable<TableRow> body, AstNode node)
{
return new Examples(tags, location, keyword, name, description, header, body);
}

protected virtual Scenario CreateScenario(Tag[] tags, Location location, string keyword, string name, string description, Step[] steps, Examples[] examples, AstNode node)
protected virtual Scenario CreateScenario(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, IEnumerable<Step> steps, IEnumerable<Examples> examples, AstNode node)
{
return new Scenario(tags, location, keyword, name, description, steps, examples);
}
Expand All @@ -214,17 +213,17 @@ protected virtual Step CreateStep(Location location, string keyword, StepKeyword
return new Step(location, keyword, keywordType, text, argument);
}

protected virtual GherkinDocument CreateGherkinDocument(Feature feature, Comment[] gherkinDocumentComments, AstNode node)
protected virtual GherkinDocument CreateGherkinDocument(Feature feature, IEnumerable<Comment> gherkinDocumentComments, AstNode node)
{
return new GherkinDocument(feature, gherkinDocumentComments);
}

protected virtual Feature CreateFeature(Tag[] tags, Location location, string language, string keyword, string name, string description, IHasLocation[] children, AstNode node)
protected virtual Feature CreateFeature(IEnumerable<Tag> tags, Location location, string language, string keyword, string name, string description, IEnumerable<IHasLocation> children, AstNode node)
{
return new Feature(tags, location, language, keyword, name, description, children);
}

protected virtual Rule CreateRule(Tag[] tags, Location location, string keyword, string name, string description, IHasLocation[] children, AstNode node)
protected virtual Rule CreateRule(IEnumerable<Tag> tags, Location location, string keyword, string name, string description, IEnumerable<IHasLocation> children, AstNode node)
{
return new Rule(tags, location, keyword, name, description, children);
}
Expand All @@ -234,17 +233,17 @@ protected virtual Tag CreateTag(Location location, string name, AstNode node)
return new Tag(location, name);
}

protected virtual Location CreateLocation(int line, int column)
protected Location CreateLocation(int line, int column)
{
return new Location(line, column);
}

protected virtual TableRow CreateTableRow(Location location, TableCell[] cells, AstNode node)
protected virtual TableRow CreateTableRow(Location location, IEnumerable<TableCell> cells, AstNode node)
{
return new TableRow(location, cells);
}

protected virtual TableCell CreateTableCell(Location location, string value)
protected TableCell CreateTableCell(Location location, string value)
{
return new TableCell(location, value);
}
Expand All @@ -254,60 +253,54 @@ private Location GetLocation(Token token, int column = 0)
return column == 0 ? token.Location : CreateLocation(token.Location.Line, column);
}

private Tag[] GetTags(AstNode node)
private IEnumerable<Tag> GetTags(AstNode node)
{
var tagsNode = node.GetSingle<AstNode>(RuleType.Tags);
if (tagsNode == null)
return [];

return tagsNode.GetTokens(TokenType.TagLine)
.SelectMany(t => t.MatchedItems, (t, tagItem) =>
CreateTag(GetLocation(t, tagItem.Column), tagItem.Text, tagsNode))
.ToArray();
}

private TableRow[] GetTableRows(AstNode node)
{
var rows = node.GetTokens(TokenType.TableRow).Select(token => CreateTableRow(GetLocation(token), GetCells(token), node)).ToArray();
CheckCellCountConsistency(rows);
return rows;
var tags = new List<Tag>();
foreach (var line in tagsNode.GetTokens(TokenType.TagLine))
{
foreach (var matchedItem in line.Line.GetTags())
tags.Add(CreateTag(GetLocation(line, matchedItem.Column), matchedItem.Text, tagsNode));
}
return tags;
}

protected virtual void CheckCellCountConsistency(TableRow[] rows)
private List<TableRow> GetTableRows(AstNode node)
{
if (rows.Length == 0)
return;

int cellCount = rows[0].Cells.Count();
for (int i = 1; i < rows.Length; i++)
var rows = new List<TableRow>();
int cellCount = 0;
bool firstRow = true;
foreach (var rowToken in node.GetTokens(TokenType.TableRow))
{
var row = rows[i];
if (row.Cells.Count() != cellCount)
var rowLocation = GetLocation(rowToken);
var cells = new List<TableCell>();
foreach (var cellItem in rowToken.Line.GetTableCells())
cells.Add(CreateTableCell(GetLocation(rowToken, cellItem.Column), cellItem.Text));
if (firstRow)
{
HandleAstError("inconsistent cell count within the table", row.Location);
cellCount = cells.Count;
firstRow = false;
}
else if (cells.Count != cellCount)
{
HandleAstError("inconsistent cell count within the table", rowLocation);
}
rows.Add(CreateTableRow(rowLocation, cells, node));
}
return rows;
}

protected virtual void HandleAstError(string message, Location location)
{
throw new AstBuilderException(message, location);
}

private TableCell[] GetCells(Token tableRowToken)
{
var cells = new TableCell[tableRowToken.MatchedItems.Length];
for (int i = 0; i < cells.Length; i++)
{
var cellItem = tableRowToken.MatchedItems[i];
cells[i] = CreateTableCell(GetLocation(tableRowToken, cellItem.Column), cellItem.Text);
}
return cells;
}

private static Step[] GetSteps(AstNode scenarioDefinitionNode)
private static List<Step> GetSteps(AstNode scenarioDefinitionNode)
{
return scenarioDefinitionNode.GetItems<Step>(RuleType.Step).ToArray();
return [..scenarioDefinitionNode.GetItems<Step>(RuleType.Step)];
}

private static string GetDescription(AstNode scenarioDefinitionNode)
Expand Down
Loading
Loading