Skip to content

Commit 9dd89cb

Browse files
committed
[.NET] GherkinLine: explicit struct Enumerator
1 parent 096e606 commit 9dd89cb

File tree

5 files changed

+173
-87
lines changed

5 files changed

+173
-87
lines changed

dotnet/Gherkin.Specs/Tokens/TestTokenFormatter.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ public string FormatToken(Token token)
77
if (token.IsEOF)
88
return "EOF";
99

10-
string stepTypeText;
10+
string stepTypeText = string.Empty;
11+
string matchedItemsText = null;
1112
switch (token.MatchedType)
1213
{
1314
case TokenType.FeatureLine:
@@ -22,13 +23,14 @@ public string FormatToken(Token token)
2223
var tokenType = token.MatchedGherkinDialect.GetStepKeywordType(token.MatchedKeyword);
2324
stepTypeText = $"({tokenType})";
2425
break;
25-
default:
26-
stepTypeText = "";
26+
case TokenType.TagLine:
27+
matchedItemsText = string.Join(",", token.Line.GetTags().Select(i => i.Column + ":" + i.Text));
28+
break;
29+
case TokenType.TableRow:
30+
matchedItemsText = string.Join(",", token.Line.GetTableCells().Select(i => i.Column + ":" + i.Text));
2731
break;
2832
}
2933

30-
var matchedItemsText = token.MatchedItems == null ? "" : string.Join(",", token.MatchedItems.Select(i => i.Column + ":" + i.Text));
31-
3234
return $"({token.Location.Line}:{token.Location.Column}){token.MatchedType}:{stepTypeText}{token.MatchedKeyword}/{token.MatchedText}/{matchedItemsText}";
3335
}
3436
}

dotnet/Gherkin/AstBuilder.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ private IEnumerable<Tag> GetTags(AstNode node)
262262
var tags = new List<Tag>();
263263
foreach (var line in tagsNode.GetTokens(TokenType.TagLine))
264264
{
265-
foreach (var matchedItem in line.MatchedItems)
265+
foreach (var matchedItem in line.Line.GetTags())
266266
tags.Add(CreateTag(GetLocation(line, matchedItem.Column), matchedItem.Text, tagsNode));
267267
}
268268
return tags;
@@ -277,7 +277,7 @@ private List<TableRow> GetTableRows(AstNode node)
277277
{
278278
var rowLocation = GetLocation(rowToken);
279279
var cells = new List<TableCell>();
280-
foreach (var cellItem in rowToken.MatchedItems)
280+
foreach (var cellItem in rowToken.Line.GetTableCells())
281281
cells.Add(CreateTableCell(GetLocation(rowToken, cellItem.Column), cellItem.Text));
282282
if (firstRow)
283283
{

dotnet/Gherkin/GherkinLine.cs

+161-75
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Gherkin.Ast;
2+
using System.Collections;
23

34
namespace Gherkin;
45

@@ -97,69 +98,131 @@ public string GetRestTrimmed(int length)
9798
return lineText.Substring(trimmedStartIndex + length).Trim();
9899
}
99100

100-
/// <summary>
101-
/// Tries parsing the line as a tag list, and returns the tags wihtout the leading '@' characters.
102-
/// </summary>
103-
/// <returns>(position,text) pairs, position is 0-based index</returns>
104-
public IEnumerable<GherkinLineSpan> GetTags()
101+
public readonly struct TagsEnumerable : IEnumerable<GherkinLineSpan>
105102
{
106-
string uncommentedLine = lineText;
107-
var commentIndex = lineText.IndexOf(GherkinLanguageConstants.COMMENT_PREFIX[0], trimmedStartIndex);
108-
while (commentIndex >= 0)
103+
readonly int lineNumber;
104+
readonly string uncommentedLine;
105+
readonly int position;
106+
public TagsEnumerable(int lineNumber, string lineText, int trimmedStartIndex)
109107
{
110-
if (commentIndex == 0)
111-
yield break;
112-
if (Array.IndexOf(inlineWhitespaceChars, lineText[commentIndex - 1]) != -1)
108+
this.lineNumber = lineNumber;
109+
uncommentedLine = lineText;
110+
var commentIndex = lineText.IndexOf(GherkinLanguageConstants.COMMENT_PREFIX[0], trimmedStartIndex);
111+
while (commentIndex >= 0)
113112
{
114-
uncommentedLine = uncommentedLine.Substring(0, commentIndex);
115-
break;
113+
if (commentIndex == 0)
114+
{
115+
position = -1;
116+
return;
117+
}
118+
if (Array.IndexOf(inlineWhitespaceChars, lineText[commentIndex - 1]) != -1)
119+
{
120+
uncommentedLine = uncommentedLine.Substring(0, commentIndex);
121+
break;
122+
}
123+
commentIndex = lineText.IndexOf(GherkinLanguageConstants.COMMENT_PREFIX[0], commentIndex + 1);
116124
}
117-
commentIndex = lineText.IndexOf(GherkinLanguageConstants.COMMENT_PREFIX[0], commentIndex + 1);
125+
position = uncommentedLine.IndexOf(GherkinLanguageConstants.TAG_PREFIX[0], trimmedStartIndex);
118126
}
119-
int position = uncommentedLine.IndexOf(GherkinLanguageConstants.TAG_PREFIX[0], trimmedStartIndex);
120-
while (position >= 0)
121-
{
122-
int nextPos = uncommentedLine.IndexOf(GherkinLanguageConstants.TAG_PREFIX[0], position + 1);
123-
int endPos;
124-
if (nextPos > 0)
125-
endPos = nextPos - 1;
126-
else
127-
endPos = uncommentedLine.Length - 1;
128-
129-
while (endPos > position && Array.IndexOf(inlineWhitespaceChars, lineText[endPos]) != -1) // TrimEnd
130-
endPos -= 1;
131-
132-
int length = endPos - position + 1;
133-
if (length <= 1)
127+
128+
public TagsEnumerator GetEnumerator() => new TagsEnumerator(lineNumber, uncommentedLine, position);
129+
130+
IEnumerator<GherkinLineSpan> IEnumerable<GherkinLineSpan>.GetEnumerator() => GetEnumerator();
131+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
132+
}
133+
134+
public struct TagsEnumerator : IEnumerator<GherkinLineSpan>
135+
{
136+
readonly int lineNumber;
137+
readonly string uncommentedLine;
138+
int position;
139+
140+
public TagsEnumerator(int lineNumber, string uncommentedLine, int position) : this()
141+
{
142+
this.lineNumber = lineNumber;
143+
this.uncommentedLine = uncommentedLine;
144+
this.position = position;
145+
}
146+
147+
public GherkinLineSpan Current { readonly get; private set; }
148+
readonly object IEnumerator.Current => Current;
149+
150+
public bool MoveNext()
151+
{
152+
while (position >= 0)
134153
{
135-
position = nextPos;
136-
continue;
137-
}
154+
int nextPos = uncommentedLine.IndexOf(GherkinLanguageConstants.TAG_PREFIX[0], position + 1);
155+
int endPos;
156+
if (nextPos > 0)
157+
endPos = nextPos - 1;
158+
else
159+
endPos = uncommentedLine.Length - 1;
160+
161+
while (endPos > position && Array.IndexOf(inlineWhitespaceChars, uncommentedLine[endPos]) != -1) // TrimEnd
162+
endPos -= 1;
163+
164+
int length = endPos - position + 1;
165+
if (length <= 1)
166+
{
167+
position = nextPos;
168+
continue;
169+
}
138170

139-
var tagName = lineText.Substring(position, length);
171+
var tagName = uncommentedLine.Substring(position, length);
140172

141-
if (tagName.IndexOfAny(inlineWhitespaceChars) >= 0)
142-
throw new InvalidTagException("A tag may not contain whitespace", new Location(LineNumber, position + 1));
173+
if (tagName.IndexOfAny(inlineWhitespaceChars) >= 0)
174+
throw new InvalidTagException("A tag may not contain whitespace", new Location(lineNumber, position + 1));
143175

144-
yield return new GherkinLineSpan(position + 1, tagName);
176+
Current = new GherkinLineSpan(position + 1, tagName);
177+
position = nextPos;
178+
return true;
179+
}
145180

146-
position = nextPos;
181+
Current = default;
182+
return false;
183+
}
184+
185+
readonly void IDisposable.Dispose()
186+
{
187+
// nothing to do
147188
}
189+
190+
void IEnumerator.Reset() => throw new NotImplementedException();
148191
}
149192

150193
/// <summary>
151-
/// Tries parsing the line as table row and returns the trimmed cell values.
194+
/// Tries parsing the line as a tag list, and returns the tags wihtout the leading '@' characters.
152195
/// </summary>
153196
/// <returns>(position,text) pairs, position is 0-based index</returns>
154-
public IEnumerable<GherkinLineSpan> GetTableCells()
197+
public TagsEnumerable GetTags() => new TagsEnumerable(LineNumber, lineText, trimmedStartIndex);
198+
199+
public readonly struct TableCellsEnumerable(string lineText, int startPos) : IEnumerable<GherkinLineSpan>
155200
{
156-
bool isFirstRow = true;
201+
public TableCellsEnumerator GetEnumerator() => new TableCellsEnumerator(lineText, startPos);
157202

158-
string cell = null;
159-
int startPos = trimmedStartIndex;
160-
int pos = startPos;
203+
IEnumerator<GherkinLineSpan> IEnumerable<GherkinLineSpan>.GetEnumerator() => GetEnumerator();
204+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
205+
}
206+
207+
public struct TableCellsEnumerator : IEnumerator<GherkinLineSpan>
208+
{
209+
readonly string lineText;
210+
int startPos;
211+
int pos;
212+
bool isFirstRow;
161213

162-
static void EnsureCellText(ref string cell, string lineText, ref int startPos, int pos, bool trim)
214+
public TableCellsEnumerator(string lineText, int startPos)
215+
{
216+
this.lineText = lineText;
217+
this.startPos = startPos;
218+
this.pos = startPos;
219+
this.isFirstRow = true;
220+
}
221+
222+
public GherkinLineSpan Current { readonly get; private set; }
223+
readonly object IEnumerator.Current => Current;
224+
225+
void EnsureCellText(ref string cell, bool trim)
163226
{
164227
if (cell is not null)
165228
{
@@ -178,53 +241,76 @@ static void EnsureCellText(ref string cell, string lineText, ref int startPos, i
178241
cell = lineText.Substring(startPos, trimedPos - startPos + 1);
179242
}
180243

181-
while (pos < lineText.Length)
244+
public bool MoveNext()
182245
{
183-
char c = lineText[pos];
184-
pos++;
185-
if (c == GherkinLanguageConstants.TABLE_CELL_SEPARATOR_CHAR)
186-
{
187-
if (isFirstRow)
188-
isFirstRow = false;
189-
else
190-
{
191-
EnsureCellText(ref cell, lineText, ref startPos, pos, true);
246+
string cell = null;
192247

193-
yield return new GherkinLineSpan(startPos + 1, cell);
194-
}
195-
cell = null;
196-
startPos = pos;
197-
}
198-
else if (c == GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR)
248+
while (pos < lineText.Length)
199249
{
200-
EnsureCellText(ref cell, lineText, ref startPos, pos, false);
201-
if ((pos + 1) < lineText.Length)
250+
char c = lineText[pos];
251+
pos++;
252+
if (c == GherkinLanguageConstants.TABLE_CELL_SEPARATOR_CHAR)
202253
{
203-
c = lineText[pos];
204-
pos++;
205-
if (c == GherkinLanguageConstants.TABLE_CELL_NEWLINE_ESCAPE)
254+
if (isFirstRow)
206255
{
207-
cell += "\n";
256+
isFirstRow = false;
257+
startPos = pos;
208258
}
209259
else
210260
{
211-
if (c != GherkinLanguageConstants.TABLE_CELL_SEPARATOR_CHAR && c != GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR)
261+
EnsureCellText(ref cell, true);
262+
263+
Current = new GherkinLineSpan(startPos + 1, cell);
264+
startPos = pos;
265+
return true;
266+
}
267+
}
268+
else if (c == GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR)
269+
{
270+
EnsureCellText(ref cell, false);
271+
if ((pos + 1) < lineText.Length)
272+
{
273+
c = lineText[pos];
274+
pos++;
275+
if (c == GherkinLanguageConstants.TABLE_CELL_NEWLINE_ESCAPE)
212276
{
213-
cell += GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR;
277+
cell += "\n";
214278
}
215-
cell += c;
279+
else
280+
{
281+
if (c != GherkinLanguageConstants.TABLE_CELL_SEPARATOR_CHAR && c != GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR)
282+
{
283+
cell += GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR;
284+
}
285+
cell += c;
286+
}
287+
}
288+
else
289+
{
290+
cell += GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR;
216291
}
217292
}
218293
else
219294
{
220-
cell += GherkinLanguageConstants.TABLE_CELL_ESCAPE_CHAR;
295+
if (cell is not null)
296+
cell += c;
221297
}
222298
}
223-
else
224-
{
225-
if (cell is not null)
226-
cell += c;
227-
}
299+
300+
return false;
228301
}
302+
303+
readonly void IDisposable.Dispose()
304+
{
305+
// nothing to do
306+
}
307+
308+
void IEnumerator.Reset() => throw new NotImplementedException();
229309
}
310+
311+
/// <summary>
312+
/// Tries parsing the line as table row and returns the trimmed cell values.
313+
/// </summary>
314+
/// <returns>(position,text) pairs, position is 0-based index</returns>
315+
public TableCellsEnumerable GetTableCells() => new TableCellsEnumerable(lineText, trimmedStartIndex);
230316
}

dotnet/Gherkin/Token.cs

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public Token(Location location)
2020
public TokenType MatchedType { get; set; }
2121
public string MatchedKeyword { get; set; }
2222
public string MatchedText { get; set; }
23-
public IEnumerable<GherkinLineSpan> MatchedItems { get; set; }
2423
public int MatchedIndent { get; set; }
2524
public GherkinDialect MatchedGherkinDialect { get; set; }
2625
public Location Location { get; set; }

dotnet/Gherkin/TokenMatcher.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,11 @@ public void Reset()
3232
currentDialect = dialectProvider.DefaultDialect;
3333
}
3434

35-
protected virtual void SetTokenMatched(Token token, TokenType matchedType, string text = null, string keyword = null, int? indent = null, IEnumerable<GherkinLineSpan> items = null)
35+
protected virtual void SetTokenMatched(Token token, TokenType matchedType, string text = null, string keyword = null, int? indent = null)
3636
{
3737
token.MatchedType = matchedType;
3838
token.MatchedKeyword = keyword;
3939
token.MatchedText = text;
40-
token.MatchedItems = items;
4140
token.MatchedGherkinDialect = CurrentDialect;
4241
token.MatchedIndent = indent ?? (token.IsEOF ? 0 : token.Line.Indent);
4342
token.Location = new Ast.Location(token.Location.Line, token.MatchedIndent + 1);
@@ -113,7 +112,7 @@ public bool Match_TagLine(Token token)
113112
{
114113
if (token.Line.StartsWith(GherkinLanguageConstants.TAG_PREFIX))
115114
{
116-
SetTokenMatched(token, TokenType.TagLine, items: token.Line.GetTags());
115+
SetTokenMatched(token, TokenType.TagLine);
117116
return true;
118117
}
119118
return false;
@@ -212,7 +211,7 @@ public bool Match_TableRow(Token token)
212211
{
213212
if (token.Line.StartsWith(GherkinLanguageConstants.TABLE_CELL_SEPARATOR))
214213
{
215-
SetTokenMatched(token, TokenType.TableRow, items: token.Line.GetTableCells());
214+
SetTokenMatched(token, TokenType.TableRow);
216215
return true;
217216
}
218217
return false;

0 commit comments

Comments
 (0)