@@ -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}
0 commit comments