Skip to content

Commit bc884b9

Browse files
authored
Enhance comment handling support (#209)
* Expand comment handling support * Address feedback
1 parent 2641cbf commit bc884b9

5 files changed

Lines changed: 704 additions & 39 deletions

File tree

SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.BeginEndBlockStatement.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public override void ExplicitVisit(BeginEndBlockStatement node)
2020
GenerateFragmentIfNotNull(node.StatementList);
2121
PopAlignmentPoint();
2222

23+
// Emit any comments sitting between the last inner statement and END.
24+
EmitCommentsUntilNextNonTriviaToken();
25+
2326
NewLine();
2427
GenerateKeyword(TSqlTokenType.End);
2528
}

SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs

Lines changed: 237 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,20 @@ internal abstract partial class SqlScriptGeneratorVisitor
3434
private bool _leadingCommentsEmitted = false;
3535

3636
/// <summary>
37-
/// When true, suppresses trailing comment emission in HandleCommentsAfterFragment
38-
/// for fragments whose LastTokenIndex matches or exceeds _suppressTrailingCommentsAfterIndex.
39-
/// Used by GenerateStatementWithSemiColon to defer trailing comments until after
40-
/// the semicolon has been placed, without affecting inter-clause comments.
37+
/// When true, defers trailing comments for fragments at or past
38+
/// _suppressTrailingCommentsAfterIndex until after the semicolon.
39+
/// Set by GenerateStatementWithSemiColon.
4140
/// </summary>
4241
private bool _suppressTrailingComments = false;
4342

43+
/// <summary>Statement boundary used by _suppressTrailingComments.</summary>
44+
private int _suppressTrailingCommentsAfterIndex = -1;
45+
4446
/// <summary>
45-
/// The LastTokenIndex of the statement for which trailing comments are being suppressed.
46-
/// Only comments after this index are suppressed.
47+
/// Buffer of '--' trailing comments awaiting the next NewLine. A '--'
48+
/// comment is only safe at end-of-line.
4749
/// </summary>
48-
private int _suppressTrailingCommentsAfterIndex = -1;
50+
private readonly List<string> _deferredTrailingSingleLineComments = new List<string>();
4951

5052
#endregion
5153

@@ -64,6 +66,7 @@ protected void SetTokenStreamForComments(IList<TSqlParserToken> tokenStream)
6466
_leadingCommentsEmitted = false;
6567
_suppressTrailingComments = false;
6668
_suppressTrailingCommentsAfterIndex = -1;
69+
_deferredTrailingSingleLineComments.Clear();
6770
}
6871

6972
/// <summary>
@@ -127,9 +130,9 @@ protected void EmitGapComments(TSqlFragment fragment)
127130
}
128131

129132
/// <summary>
130-
/// Emits trailing comments that appear immediately after the fragment.
133+
/// Emits trailing comments after the fragment, scanning across newlines.
134+
/// Each comment's own-line vs same-line placement is preserved from source.
131135
/// </summary>
132-
/// <param name="fragment">The fragment that was just generated.</param>
133136
protected void EmitTrailingComments(TSqlFragment fragment)
134137
{
135138
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
@@ -143,23 +146,229 @@ protected void EmitTrailingComments(TSqlFragment fragment)
143146
return;
144147
}
145148

146-
// Scan for comments immediately following the fragment
149+
int prevEmittedSourceIndex = lastTokenIndex;
147150
for (int i = lastTokenIndex + 1; i < _currentTokenStream.Count; i++)
148151
{
149152
var token = _currentTokenStream[i];
150-
151-
if (IsCommentToken(token) && !_emittedComments.Contains(token))
153+
154+
if (IsCommentToken(token))
152155
{
153-
EmitCommentToken(token, isLeading: false);
154-
_emittedComments.Add(token);
155-
_lastProcessedTokenIndex = i;
156+
if (!_emittedComments.Contains(token))
157+
{
158+
bool ownLine = SourceGapContainsNewline(prevEmittedSourceIndex, i);
159+
EmitTrailingCommentToken(token, ownLine);
160+
_emittedComments.Add(token);
161+
_lastProcessedTokenIndex = i;
162+
prevEmittedSourceIndex = i;
163+
}
164+
continue;
165+
}
166+
167+
if (token.TokenType == TSqlTokenType.WhiteSpace)
168+
{
169+
continue;
170+
}
171+
172+
// Any other token (including ';') ends the window.
173+
break;
174+
}
175+
}
176+
177+
/// <summary>
178+
/// Trailing-comment scan limited to the fragment's last source line.
179+
/// Used after statement-ending semicolons so a comment on a later line
180+
/// remains a leading comment of the next statement.
181+
/// </summary>
182+
protected void EmitSameLineTrailingComments(TSqlFragment fragment)
183+
{
184+
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
185+
{
186+
return;
187+
}
188+
189+
int lastTokenIndex = fragment.LastTokenIndex;
190+
if (lastTokenIndex < 0 || lastTokenIndex >= _currentTokenStream.Count)
191+
{
192+
return;
193+
}
194+
195+
for (int i = lastTokenIndex + 1; i < _currentTokenStream.Count; i++)
196+
{
197+
var token = _currentTokenStream[i];
198+
199+
if (token.TokenType == TSqlTokenType.WhiteSpace)
200+
{
201+
if (ContainsLineBreak(token.Text))
202+
{
203+
break;
204+
}
205+
continue;
156206
}
157-
else if (token.TokenType != TSqlTokenType.WhiteSpace)
207+
208+
if (IsCommentToken(token))
209+
{
210+
if (!_emittedComments.Contains(token))
211+
{
212+
EmitTrailingCommentToken(token, ownLine: false);
213+
_emittedComments.Add(token);
214+
_lastProcessedTokenIndex = i;
215+
216+
// A '--' comment or a newline-spanning '/* */' ends the line.
217+
if (token.TokenType == TSqlTokenType.SingleLineComment ||
218+
ContainsLineBreak(token.Text))
219+
{
220+
break;
221+
}
222+
}
223+
continue;
224+
}
225+
226+
break;
227+
}
228+
}
229+
230+
/// <summary>True if any whitespace token between fromIndex and toIndex contains a line break.</summary>
231+
private bool SourceGapContainsNewline(int fromIndex, int toIndex)
232+
{
233+
for (int j = fromIndex + 1; j < toIndex; j++)
234+
{
235+
var t = _currentTokenStream[j];
236+
if (t.TokenType == TSqlTokenType.WhiteSpace && ContainsLineBreak(t.Text))
158237
{
159-
// Stop at next non-whitespace, non-comment token
160-
break;
238+
return true;
161239
}
162240
}
241+
return false;
242+
}
243+
244+
/// <summary>
245+
/// Emits any unemitted comments whose token index falls within the
246+
/// statement's source token range (up to and including LastTokenIndex).
247+
/// Catches floating comments inside a statement whose '/' or ';' has been
248+
/// absorbed into this statement (e.g. '/* */;' or leading ';WITH').
249+
/// </summary>
250+
protected void EmitUnemittedCommentsThroughStatementEnd(TSqlStatement statement)
251+
{
252+
if (!_options.PreserveComments || _currentTokenStream == null || statement == null)
253+
{
254+
return;
255+
}
256+
257+
int endInclusive = statement.LastTokenIndex;
258+
if (endInclusive < 0 || endInclusive >= _currentTokenStream.Count)
259+
{
260+
return;
261+
}
262+
263+
for (int i = _lastProcessedTokenIndex + 1; i <= endInclusive; i++)
264+
{
265+
var t = _currentTokenStream[i];
266+
if (IsCommentToken(t) && !_emittedComments.Contains(t))
267+
{
268+
EmitTrailingCommentToken(t, ownLine: true);
269+
_emittedComments.Add(t);
270+
}
271+
}
272+
273+
if (endInclusive > _lastProcessedTokenIndex)
274+
{
275+
_lastProcessedTokenIndex = endInclusive;
276+
}
277+
}
278+
279+
/// <summary>
280+
/// Emits unemitted comments in the trivia run starting at
281+
/// _lastProcessedTokenIndex+1; stops at the first non-whitespace,
282+
/// non-comment token. For use before a container emits a closing
283+
/// keyword like END.
284+
/// </summary>
285+
protected void EmitCommentsUntilNextNonTriviaToken()
286+
{
287+
if (!_options.PreserveComments || _currentTokenStream == null)
288+
{
289+
return;
290+
}
291+
292+
for (int i = _lastProcessedTokenIndex + 1; i < _currentTokenStream.Count; i++)
293+
{
294+
var t = _currentTokenStream[i];
295+
296+
if (IsCommentToken(t))
297+
{
298+
if (!_emittedComments.Contains(t))
299+
{
300+
EmitTrailingCommentToken(t, ownLine: true);
301+
_emittedComments.Add(t);
302+
_lastProcessedTokenIndex = i;
303+
}
304+
continue;
305+
}
306+
307+
if (t.TokenType == TSqlTokenType.WhiteSpace)
308+
{
309+
continue;
310+
}
311+
312+
break;
313+
}
314+
}
315+
316+
/// <summary>
317+
/// Emits a trailing comment. '--' comments are deferred to the next
318+
/// NewLine; block comments are written inline immediately.
319+
/// </summary>
320+
private void EmitTrailingCommentToken(TSqlParserToken token, bool ownLine)
321+
{
322+
if (token == null)
323+
{
324+
return;
325+
}
326+
327+
if (token.TokenType == TSqlTokenType.SingleLineComment)
328+
{
329+
_deferredTrailingSingleLineComments.Add(token.Text);
330+
return;
331+
}
332+
333+
if (ownLine)
334+
{
335+
_writer.NewLine();
336+
}
337+
else
338+
{
339+
_writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1));
340+
}
341+
342+
_writer.AddToken(new TSqlParserToken(token.TokenType, token.Text));
343+
}
344+
345+
/// <summary>
346+
/// Writes deferred '--' trailing comments at end-of-line. Called from
347+
/// the visitor's NewLine helper before each newline, and at end-of-script.
348+
/// </summary>
349+
internal void FlushDeferredTrailingSingleLineComments()
350+
{
351+
if (_deferredTrailingSingleLineComments.Count == 0)
352+
{
353+
return;
354+
}
355+
356+
for (int i = 0; i < _deferredTrailingSingleLineComments.Count; i++)
357+
{
358+
_writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1));
359+
_writer.AddToken(new TSqlParserToken(
360+
TSqlTokenType.SingleLineComment,
361+
_deferredTrailingSingleLineComments[i]));
362+
363+
// The final '--' is terminated by the caller's pending newline;
364+
// earlier ones need their own.
365+
if (i < _deferredTrailingSingleLineComments.Count - 1)
366+
{
367+
_writer.NewLine();
368+
}
369+
}
370+
371+
_deferredTrailingSingleLineComments.Clear();
163372
}
164373

165374
/// <summary>
@@ -208,17 +417,13 @@ protected void HandleCommentsAfterFragment(TSqlFragment fragment)
208417
return;
209418
}
210419

211-
// When trailing comments are suppressed (e.g., during statement body generation
212-
// before semicolon placement), skip emitting trailing comments only for fragments
213-
// whose last token is at or past the statement boundary. Inter-clause comments
214-
// (within the statement) are still emitted normally.
420+
// Defer until after the semicolon when at statement boundary.
215421
if (_suppressTrailingComments && fragment.LastTokenIndex >= _suppressTrailingCommentsAfterIndex)
216422
{
217423
UpdateLastProcessedIndex(fragment);
218424
return;
219425
}
220426

221-
// Emit trailing comments and update tracking
222427
EmitTrailingComments(fragment);
223428
UpdateLastProcessedIndex(fragment);
224429
}
@@ -276,6 +481,9 @@ private void EmitCommentToken(TSqlParserToken token, bool isLeading)
276481
/// </summary>
277482
protected void EmitRemainingComments()
278483
{
484+
// Flush deferred '--' comments at end-of-script.
485+
FlushDeferredTrailingSingleLineComments();
486+
279487
if (!_options.PreserveComments || _currentTokenStream == null)
280488
{
281489
return;
@@ -305,6 +513,12 @@ private static bool IsCommentToken(TSqlParserToken token)
305513
token.TokenType == TSqlTokenType.MultilineComment);
306514
}
307515

516+
/// <summary>True if the text contains '\n' or '\r'.</summary>
517+
private static bool ContainsLineBreak(string text)
518+
{
519+
return text != null && (text.IndexOf('\n') >= 0 || text.IndexOf('\r') >= 0);
520+
}
521+
308522
#endregion
309523
}
310524
}

SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,11 +473,25 @@ protected void GenerateStatementWithSemiColon(TSqlStatement statement)
473473
_suppressTrailingComments = previousSuppressState;
474474
_suppressTrailingCommentsAfterIndex = previousSuppressIndex;
475475

476+
// Sweep any comments inside the statement's token range that no
477+
// inner-fragment scan emitted (e.g. comments between an absorbed
478+
// ';' separator and the statement's last token).
479+
EmitUnemittedCommentsThroughStatementEnd(statement);
480+
476481
// Semicolon BEFORE trailing comments
477482
GenerateSemiColonWhenNecessary(statement);
478483

479-
// Now emit trailing comments (after the semicolon)
480-
HandleCommentsAfterFragment(statement);
484+
// Only same-line trailing comments belong after the semicolon; a
485+
// comment on a later line is a leading comment of the next statement.
486+
if (_options.PreserveComments && _currentTokenStream != null)
487+
{
488+
EmitSameLineTrailingComments(statement);
489+
UpdateLastProcessedIndex(statement);
490+
}
491+
else
492+
{
493+
HandleCommentsAfterFragment(statement);
494+
}
481495
}
482496

483497
protected void GenerateCommaSeparatedWithClause<T>(IList<T> fragments, bool indent, bool includeParentheses) where T : TSqlFragment

SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ protected void Mark(AlignmentPoint ap)
7070

7171
protected void NewLine()
7272
{
73+
if (_options.PreserveComments)
74+
{
75+
FlushDeferredTrailingSingleLineComments();
76+
}
7377
_writer.NewLine();
7478
}
7579

0 commit comments

Comments
 (0)