1- using SqlServerSimulator . Parser . Tokens ;
1+ using SqlServerSimulator . Parser . Tokens ;
22using SqlServerSimulator . Storage ;
33using 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>
1026static 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}
0 commit comments