Skip to content

Commit 3990c7a

Browse files
committed
Minor refactoring for quality improvement.
1 parent 0493c06 commit 3990c7a

8 files changed

Lines changed: 240 additions & 208 deletions

File tree

SqlServerSimulator.Tests.Internal/Parser/TokenLineNumberTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public void NewlineWithinACommentToken_PushesLaterTokensDown()
7676
/// </summary>
7777
private static IEnumerable<Token> TokenizeMeaningful(string command)
7878
{
79-
var index = -1;
79+
var index = 0;
8080
while (Tokenizer.NextToken(command, ref index) is Token t)
8181
{
8282
if (t is Whitespace or Comment)

SqlServerSimulator/Parser/BooleanExpression.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ private protected BooleanExpression(Expression left, ParserContext context)
2121
{
2222
}
2323

24+
/// <summary>
25+
/// Parses a comparison operator and its right-hand expression. Follows
26+
/// the lookahead contract documented on <see cref="ParserContext"/>: on
27+
/// return, <see cref="ParserContext.Token"/> is the first token not
28+
/// consumed by the comparison.
29+
/// </summary>
2430
public static BooleanExpression Parse(Expression left, ParserContext context) => context.Token switch
2531
{
2632
Operator { Character: '=' } => new EqualityExpression(left, context),

SqlServerSimulator/Parser/Expression.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ private protected Expression()
2626
public virtual byte Precedence => 0;
2727

2828
/// <summary>
29-
/// Converts the tokens from a command into a single expression.
29+
/// Converts the tokens from a command into a single expression. Follows
30+
/// the lookahead contract documented on <see cref="ParserContext"/>: on
31+
/// return, <see cref="ParserContext.Token"/> is the first token not
32+
/// consumed by the parsed expression.
3033
/// </summary>
3134
/// <param name="context">Manages the overall parsing state.</param>
3235
/// <returns>The parsed expression.</returns>

SqlServerSimulator/Parser/ParserContext.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,26 @@ namespace SqlServerSimulator.Parser;
1111
/// <summary>
1212
/// Organizes relevant information for parsing of SQL commands.
1313
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// <b>Lookahead contract.</b> Every <c>Parse</c>-style helper in this
17+
/// namespace (e.g. <see cref="Expression.Parse(ParserContext)"/>,
18+
/// <see cref="Selection.Parse(ParserContext, uint)"/>,
19+
/// <see cref="BooleanExpression.Parse"/>) leaves <see cref="Token"/> at the
20+
/// first token it did <i>not</i> consume — its caller's lookahead position.
21+
/// A helper that reads up to and including a closing delimiter (e.g. a
22+
/// function call's <c>)</c>) leaves <see cref="Token"/> on that delimiter;
23+
/// the surrounding loop's next <see cref="GetNextOptional"/> /
24+
/// <see cref="MoveNext"/> advances past it. Callers must not "step back" or
25+
/// "step forward" to re-align after a Parse returns.
26+
/// </para>
27+
/// <para>
28+
/// This contract is what makes recursive descent compose. Violations show up
29+
/// as silently dropped tokens. When in doubt, read a token at the call site,
30+
/// decide whether to consume it, and never assume a previous Parse left the
31+
/// cursor "before" or "after" something the contract didn't promise.
32+
/// </para>
33+
/// </remarks>
1434
internal sealed class ParserContext(SimulatedDbCommand command)
1535
{
1636
#pragma warning disable CA2213 // Disposable fields should be disposed
@@ -22,9 +42,11 @@ internal sealed class ParserContext(SimulatedDbCommand command)
2242
command.CommandText;
2343

2444
/// <summary>
25-
/// The tokenizer position within <see cref="commandText"/>.
45+
/// The tokenizer position within <see cref="commandText"/>: the next
46+
/// un-read character. <see cref="MoveNext"/> advances this past the
47+
/// returned token (see <see cref="Tokenizer"/>'s index contract).
2648
/// </summary>
27-
private int index = -1;
49+
private int index;
2850

2951
/// <summary>
3052
/// The most recently identified token in the command string.
@@ -200,11 +222,9 @@ public override string ToString()
200222
{
201223
var command = this.commandText;
202224
Span<char> result = stackalloc char[command.Length + 2];
203-
if (index < 0)
225+
if (this.Token is { } token)
204226
{
205-
result[0] = '»';
206-
result[1] = '«';
207-
command.CopyTo(result[2..]);
227+
token.Highlight(result);
208228
}
209229
else if (index >= command.Length)
210230
{
@@ -214,8 +234,10 @@ public override string ToString()
214234
}
215235
else
216236
{
217-
System.Diagnostics.Debug.Assert(this.Token is not null);
218-
this.Token.Highlight(result);
237+
// Pre-MoveNext state: cursor at the start.
238+
result[0] = '»';
239+
result[1] = '«';
240+
command.CopyTo(result[2..]);
219241
}
220242

221243
return new string(result);

SqlServerSimulator/Parser/Selection.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ internal sealed class Selection
1313
private Selection(SimulatedQueryResult results) => this.Results = results;
1414

1515
/// <summary>
16-
/// Creates a <see cref="Selection"/> from a series of tokens.
16+
/// Creates a <see cref="Selection"/> from a series of tokens. Follows the
17+
/// lookahead contract documented on <see cref="ParserContext"/>: on
18+
/// return, <see cref="ParserContext.Token"/> is the first token not
19+
/// consumed by the SELECT (typically <c>;</c>, <c>)</c> for a derived
20+
/// table, or null at end of command).
1721
/// </summary>
1822
/// <param name="context">Manages the overall parsing state.</param>
1923
/// <param name="depth">The current depth of recursed selection, such as with derived tables. 0 for the top-level SELECT.</param>

SqlServerSimulator/Parser/Tokenizer.cs

Lines changed: 47 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using SqlServerSimulator.Parser.Tokens;
1+
using SqlServerSimulator.Parser.Tokens;
22
using SqlServerSimulator.Storage;
33
using System.Text;
44

@@ -7,17 +7,33 @@ namespace SqlServerSimulator.Parser;
77
/// <summary>
88
/// Specializes in refining a SQL command string into sequence of <see cref="Token"/> instances.
99
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// <b>Index contract.</b> Every parse function (and <see cref="NextToken"/>
13+
/// itself) leaves <c>index</c> at the position of the next un-read character
14+
/// — one past the last character consumed by the returned token. Callers
15+
/// never need to "step back" or "step forward" to re-align: invoking
16+
/// <see cref="NextToken"/> again with the same <c>ref index</c> reads the
17+
/// next token. To begin tokenizing a new command, start with <c>index = 0</c>.
18+
/// </para>
19+
/// <para>
20+
/// This invariant is what lets each parser end with <c>index - start</c> for
21+
/// length math, with no off-by-one. Adding a new token type? Scan forward
22+
/// while characters match, stop at the first that doesn't, return — the
23+
/// natural exit position of that loop is already correct.
24+
/// </para>
25+
/// </remarks>
1026
static class Tokenizer
1127
{
1228
/// <summary>
1329
/// Provides the next <see cref="Token"/> from the provided SQL command text beginning at <paramref name="index"/>.
1430
/// </summary>
1531
/// <param name="command">The command from which a token is produced.</param>
16-
/// <param name="index">The position within <paramref name="command"/> of the previous token (or -1), updated to where the next token ends.</param>
32+
/// <param name="index">The position of the next un-read character (0 to begin); updated to the next un-read position past the returned token.</param>
1733
/// <returns>The next token, or null if the end of <paramref name="command"/> has been reached.</returns>
1834
/// <exception cref="SimulatedSqlException">Incorrect or unsupported syntax.</exception>
1935
public static Token? NextToken(string command, ref int index) =>
20-
++index >= command.Length ? null : command[index] switch
36+
index >= command.Length ? null : command[index] switch
2137
{
2238
' ' or '\r' or '\n' or '\t' => ParseWhitespace(command, ref index),
2339
'N' or 'n' when index + 1 < command.Length && command[index + 1] == '\'' => ParseNPrefixedStringLiteral(command, ref index),
@@ -29,28 +45,18 @@ static class Tokenizer
2945
'-' => ParseMinusOrComment(command, ref index),
3046
'/' => ParseForwardSlashOrComment(command, ref index),
3147
'[' => ParseBracketDelimitedString(command, ref index),
32-
'+' or '*' or '%' or '(' or ')' or ',' or '.' or ';' or '=' or '&' or '|' or '^' or '>' or '<' or '!' => new Operator(command, index),
48+
'+' or '*' or '%' or '(' or ')' or ',' or '.' or ';' or '=' or '&' or '|' or '^' or '>' or '<' or '!' => new Operator(command, index++),
3349
var c => throw SimulatedSqlException.SyntaxErrorNear(c) // Might throw on valid-but-unsupported syntax.
3450
};
3551

3652
private static Whitespace ParseWhitespace(string command, ref int index)
3753
{
3854
var start = index;
39-
while (++index < command.Length)
55+
while (++index < command.Length && command[index] is ' ' or '\r' or '\n' or '\t')
4056
{
41-
switch (command[index])
42-
{
43-
case ' ':
44-
case '\r':
45-
case '\n':
46-
case '\t':
47-
continue;
48-
}
49-
50-
break;
5157
}
5258

53-
return new(command, start, index-- - start);
59+
return new(command, start, index - start);
5460
}
5561

5662
private static Token ParseUnquotedStringOrReservedKeyword(string command, ref int index)
@@ -59,28 +65,21 @@ private static Token ParseUnquotedStringOrReservedKeyword(string command, ref in
5965
while (++index < command.Length)
6066
{
6167
var c = command[index];
62-
63-
if (char.IsLetterOrDigit(c) || c == '_')
64-
continue;
65-
66-
break;
68+
if (!char.IsLetterOrDigit(c) && c != '_')
69+
break;
6770
}
6871

69-
return UnquotedString.CheckReserved(command, start, index-- - start);
72+
return UnquotedString.CheckReserved(command, start, index - start);
7073
}
7174

7275
private static Numeric ParseNumeric(string command, ref int index)
7376
{
7477
var start = index;
75-
while (++index < command.Length)
78+
while (++index < command.Length && command[index] is >= '0' and <= '9')
7679
{
77-
if (command[index] is >= '0' and <= '9')
78-
continue;
79-
80-
break;
8180
}
8281

83-
return new(command, start, index-- - start);
82+
return new(command, start, index - start);
8483
}
8584

8685
private static Token ParseAtOrDoubleAtPrefixedString(string command, ref int index)
@@ -100,47 +99,33 @@ private static Token ParseAtOrDoubleAtPrefixedString(string command, ref int ind
10099
doubleAt = false;
101100
}
102101

103-
while (++index < command.Length)
102+
while (index < command.Length)
104103
{
105104
var c = command[index];
106-
107-
if (char.IsLetterOrDigit(c) || c == '_')
108-
continue;
109-
110-
break;
105+
if (!char.IsLetterOrDigit(c) && c != '_')
106+
break;
107+
index++;
111108
}
112109

113110
return doubleAt ?
114-
new DoubleAtPrefixedString(command, start, index-- - start) :
115-
new AtPrefixedString(command, start, index-- - start);
111+
new DoubleAtPrefixedString(command, start, index - start) :
112+
new AtPrefixedString(command, start, index - start);
116113
}
117114

118115
private static Token ParseMinusOrComment(string command, ref int index)
119116
{
120117
var start = index;
121-
if (++index == command.Length)
122-
return new Operator(command, --index);
123-
if (command[index] != '-')
124-
{
125-
index--;
126-
return new Operator(command, index);
127-
}
128-
129-
return Comment.ParseSingleLine(start, ref index, command);
118+
return ++index == command.Length || command[index] != '-'
119+
? new Operator(command, start)
120+
: Comment.ParseSingleLine(start, ref index, command);
130121
}
131122

132123
private static Token ParseForwardSlashOrComment(string command, ref int index)
133124
{
134125
var start = index;
135-
if (++index == command.Length)
136-
return new Operator(command, --index);
137-
if (command[index] != '*')
138-
{
139-
index--;
140-
return new Operator(command, index);
141-
}
142-
143-
return Comment.ParseBlock(start, ref index, command);
126+
return ++index == command.Length || command[index] != '*'
127+
? new Operator(command, start)
128+
: Comment.ParseBlock(start, ref index, command);
144129
}
145130

146131
/// <summary>
@@ -168,7 +153,7 @@ private static Literal ParseStringLiteral(string command, ref int index)
168153
continue;
169154
}
170155

171-
return new Literal(SqlValue.FromVarchar(builder.ToString()), command, start, index - start + 1);
156+
return new Literal(SqlValue.FromVarchar(builder.ToString()), command, start, ++index - start);
172157
}
173158

174159
throw SimulatedSqlException.UnclosedStringLiteral();
@@ -201,7 +186,7 @@ private static Literal ParseNPrefixedStringLiteral(string command, ref int index
201186
continue;
202187
}
203188

204-
return new Literal(SqlValue.FromNVarchar(builder.ToString()), command, start, index - start + 1);
189+
return new Literal(SqlValue.FromNVarchar(builder.ToString()), command, start, ++index - start);
205190
}
206191

207192
throw SimulatedSqlException.UnclosedStringLiteral();
@@ -218,8 +203,7 @@ private static Literal ParseNPrefixedStringLiteral(string command, ref int index
218203
private static Literal ParseHexLiteral(string command, ref int index)
219204
{
220205
var start = index;
221-
index++; // skip '0'
222-
index++; // skip 'x' / 'X'
206+
index += 2; // skip '0x' / '0X'
223207
var bodyStart = index;
224208
while (index < command.Length && IsHexDigit(command[index]))
225209
index++;
@@ -229,8 +213,7 @@ private static Literal ParseHexLiteral(string command, ref int index)
229213
: bodyLength % 2 == 0 ? Convert.FromHexString(command.AsSpan(bodyStart, bodyLength))
230214
: Convert.FromHexString(string.Concat("0", command.AsSpan(bodyStart, bodyLength)));
231215

232-
index--; // outer dispatch advances past the last consumed char
233-
return new Literal(SqlValue.FromVarbinary(bytes), command, start, index - start + 1);
216+
return new Literal(SqlValue.FromVarbinary(bytes), command, start, index - start);
234217
}
235218

236219
private static bool IsHexDigit(char c) =>
@@ -259,6 +242,9 @@ private static BracketDelimitedString ParseBracketDelimitedString(string command
259242
break;
260243
}
261244

262-
return new(builder.ToString(), command, start, index - start);
245+
var length = index - start;
246+
if (index < command.Length)
247+
index++;
248+
return new(builder.ToString(), command, start, length);
263249
}
264250
}

SqlServerSimulator/Parser/Tokens/Comment.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace SqlServerSimulator.Parser.Tokens;
1+
namespace SqlServerSimulator.Parser.Tokens;
22

33
sealed class Comment : Token
44
{
@@ -10,34 +10,28 @@ private Comment(string command, int index, int length) : base(command, index, le
1010
}
1111

1212
/// <summary>
13-
/// Parses a single-line comment (`-- ...`) where the initial `--` has already been consumed.
13+
/// Parses a single-line comment (<c>-- ...</c>) where the initial <c>--</c> has already been consumed.
1414
/// </summary>
15-
/// <param name="start">The position of the `--`.</param>
16-
/// <param name="index">Initially after the opening `--`, updated to just after the end of the line.</param>
15+
/// <param name="start">The position of the <c>--</c>.</param>
16+
/// <param name="index">Initially after the opening <c>--</c>; updated to the next un-read character past the comment (the line break, or end of command).</param>
1717
/// <param name="command">The raw command to parse.</param>
1818
/// <returns>A <see cref="Comment"/>.</returns>
1919
public static Comment ParseSingleLine(int start, ref int index, string command)
2020
{
2121
while (++index < command.Length)
2222
{
23-
switch (command[index])
24-
{
25-
case '\r':
26-
case '\n':
27-
return new Comment(command, start, index-- - start);
28-
}
23+
if (command[index] is '\r' or '\n')
24+
return new Comment(command, start, index - start);
2925
}
3026

31-
index--;
32-
3327
return new Comment(command, start, 2);
3428
}
3529

3630
/// <summary>
37-
/// Parses a block-style comment (`/* ... */`) where the initial `/*` has already been consumed.
31+
/// Parses a block-style comment (<c>/* ... */</c>) where the initial <c>/*</c> has already been consumed.
3832
/// </summary>
39-
/// <param name="start">The position of the opening `/`.</param>
40-
/// <param name="index">Initially after the opening `*`, updated to just after the closing `/`.</param>
33+
/// <param name="start">The position of the opening <c>/</c>.</param>
34+
/// <param name="index">Initially after the opening <c>*</c>; updated to the next un-read character past the closing <c>*/</c>.</param>
4135
/// <param name="command">The raw command to parse.</param>
4236
/// <returns>A <see cref="Comment"/>.</returns>
4337
/// <exception cref="SimulatedSqlException">Missing end comment mark '*/'.</exception>
@@ -56,7 +50,10 @@ public static Comment ParseBlock(int start, ref int index, string command)
5650
if (command[index + 1] == '/')
5751
{
5852
if (depth == 0)
59-
return new Comment(command, start, ++index - start + 1);
53+
{
54+
index += 2;
55+
return new Comment(command, start, index - start);
56+
}
6057

6158
depth--;
6259
}

0 commit comments

Comments
 (0)