Skip to content

Commit

Permalink
[.NET] Avoid allocation and improve parsing time (#344)
Browse files Browse the repository at this point in the history
* [.NET] Change some types from class to struct

* [.NET] GherkinLine: remove trimmedLineText

* [.NET] GherkinLine.GetTableCells: avoid extra TrimEnd allocation

* [.NET] Optimize AstNode.subitems with simple List and custom Enumerator

* [.NET] Replace use of arrays with Enumerable

* [.NET] GherkinLine: explicit struct Enumerator
  • Loading branch information
obligaron authored Feb 5, 2025
1 parent 5b54a36 commit cf4160f
Show file tree
Hide file tree
Showing 24 changed files with 391 additions and 302 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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

### Fixed
- [c] slight update to existing CMakeFiles.txt to propagate VERSION. Close #320 ([#328](https://github.com/cucumber/gherkin/pull/328))
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

0 comments on commit cf4160f

Please sign in to comment.