@@ -23,6 +23,16 @@ internal abstract partial class SqlScriptGeneratorVisitor
2323 /// </summary>
2424 private IList < TSqlParserToken > _currentTokenStream ;
2525
26+ /// <summary>
27+ /// Tracks which comment tokens have already been emitted to avoid duplicates.
28+ /// </summary>
29+ private readonly HashSet < TSqlParserToken > _emittedComments = new HashSet < TSqlParserToken > ( ) ;
30+
31+ /// <summary>
32+ /// Tracks whether leading (file-level) comments have been emitted.
33+ /// </summary>
34+ private bool _leadingCommentsEmitted = false ;
35+
2636 #endregion
2737
2838 #region Comment Preservation Methods
@@ -36,216 +46,226 @@ protected void SetTokenStreamForComments(IList<TSqlParserToken> tokenStream)
3646 {
3747 _currentTokenStream = tokenStream ;
3848 _lastProcessedTokenIndex = - 1 ;
49+ _emittedComments . Clear ( ) ;
50+ _leadingCommentsEmitted = false ;
3951 }
4052
4153 /// <summary>
42- /// Gets leading comments that appear between the last processed token and the current fragment.
54+ /// Emits comments that appear before the first fragment in the script (file-level leading comments).
55+ /// Called once when generating the first fragment.
4356 /// </summary>
44- /// <param name="fragment">The current fragment being visited.</param>
45- /// <returns>List of comment information for leading comments.</returns>
46- protected List < CommentInfo > GetLeadingComments ( TSqlFragment fragment )
57+ /// <param name="fragment">The first fragment being generated.</param>
58+ protected void EmitLeadingComments ( TSqlFragment fragment )
4759 {
48- var comments = new List < CommentInfo > ( ) ;
49-
50- if ( _currentTokenStream == null || fragment == null || ! _options . PreserveComments )
60+ if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
5161 {
52- return comments ;
62+ return ;
5363 }
5464
55- int startIndex = _lastProcessedTokenIndex + 1 ;
56- int endIndex = fragment . FirstTokenIndex ;
65+ if ( fragment . FirstTokenIndex <= 0 )
66+ {
67+ return ;
68+ }
5769
58- // Scan for comments between last processed and current fragment
59- for ( int i = startIndex ; i < endIndex && i < _currentTokenStream . Count ; i ++ )
70+ for ( int i = 0 ; i < fragment . FirstTokenIndex && i < _currentTokenStream . Count ; i ++ )
6071 {
6172 var token = _currentTokenStream [ i ] ;
62- if ( IsCommentToken ( token ) )
73+ if ( IsCommentToken ( token ) && ! _emittedComments . Contains ( token ) )
6374 {
64- comments . Add ( new CommentInfo ( token , CommentPosition . Leading , fragment . FirstTokenIndex ) ) ;
75+ EmitCommentToken ( token , isLeading : true ) ;
76+ _emittedComments . Add ( token ) ;
6577 }
6678 }
67-
68- return comments ;
6979 }
7080
7181 /// <summary>
72- /// Gets trailing comments that appear on the same line after the fragment.
82+ /// Emits comments that appear in the gap between the last emitted token and the current fragment.
83+ /// This captures comments embedded within sub-expressions.
7384 /// </summary>
74- /// <param name="fragment">The current fragment being visited.</param>
75- /// <returns>List of comment information for trailing comments.</returns>
76- protected List < CommentInfo > GetTrailingComments ( TSqlFragment fragment )
85+ /// <param name="fragment">The fragment about to be generated.</param>
86+ protected void EmitGapComments ( TSqlFragment fragment )
7787 {
78- var comments = new List < CommentInfo > ( ) ;
79-
80- if ( _currentTokenStream == null || fragment == null || ! _options . PreserveComments )
88+ if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
8189 {
82- return comments ;
90+ return ;
8391 }
8492
85- int lastTokenIndex = fragment . LastTokenIndex ;
86- if ( lastTokenIndex < 0 || lastTokenIndex >= _currentTokenStream . Count )
93+ int startIndex = _lastProcessedTokenIndex + 1 ;
94+ int endIndex = fragment . FirstTokenIndex ;
95+
96+ if ( endIndex <= startIndex )
8797 {
88- return comments ;
98+ return ;
8999 }
90100
91- var lastToken = _currentTokenStream [ lastTokenIndex ] ;
92- int lastTokenLine = lastToken . Line ;
93-
94- // Scan for comments after the last token on the same line
95- for ( int i = lastTokenIndex + 1 ; i < _currentTokenStream . Count ; i ++ )
101+ for ( int i = startIndex ; i < endIndex && i < _currentTokenStream . Count ; i ++ )
96102 {
97103 var token = _currentTokenStream [ i ] ;
98-
99- // Stop if we've gone past the same line (unless it's whitespace with no newline)
100- if ( token . Line > lastTokenLine )
101- {
102- break ;
103- }
104-
105- // Found a comment on the same line
106- if ( IsCommentToken ( token ) )
104+ if ( IsCommentToken ( token ) && ! _emittedComments . Contains ( token ) )
107105 {
108- comments . Add ( new CommentInfo ( token , CommentPosition . Trailing , lastTokenIndex ) ) ;
109-
110- // For single-line comments, there can't be anything else after on this line
111- if ( token . TokenType == TSqlTokenType . SingleLineComment )
112- {
113- break ;
114- }
106+ EmitCommentToken ( token , isLeading : true ) ;
107+ _emittedComments . Add ( token ) ;
108+ _lastProcessedTokenIndex = i ;
115109 }
116110 }
117-
118- return comments ;
119111 }
120112
121113 /// <summary>
122- /// Emits a comment using the script writer with current indentation .
114+ /// Emits trailing comments that appear immediately after the fragment .
123115 /// </summary>
124- /// <param name="commentInfo ">The comment to emit .</param>
125- protected void EmitComment ( CommentInfo commentInfo )
116+ /// <param name="fragment ">The fragment that was just generated .</param>
117+ protected void EmitTrailingComments ( TSqlFragment fragment )
126118 {
127- if ( commentInfo == null || commentInfo . Token == null )
119+ if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
128120 {
129121 return ;
130122 }
131123
132- var text = commentInfo . Text ;
133- if ( string . IsNullOrEmpty ( text ) )
124+ int lastTokenIndex = fragment . LastTokenIndex ;
125+ if ( lastTokenIndex < 0 || lastTokenIndex >= _currentTokenStream . Count )
134126 {
135127 return ;
136128 }
137129
138- if ( commentInfo . IsSingleLineComment )
139- {
140- EmitSingleLineComment ( text , commentInfo . Position ) ;
141- }
142- else if ( commentInfo . IsMultiLineComment )
130+ // Scan for comments immediately following the fragment
131+ for ( int i = lastTokenIndex + 1 ; i < _currentTokenStream . Count ; i ++ )
143132 {
144- EmitMultiLineComment ( text , commentInfo . Position ) ;
133+ var token = _currentTokenStream [ i ] ;
134+
135+ if ( IsCommentToken ( token ) && ! _emittedComments . Contains ( token ) )
136+ {
137+ EmitCommentToken ( token , isLeading : false ) ;
138+ _emittedComments . Add ( token ) ;
139+ _lastProcessedTokenIndex = i ;
140+ }
141+ else if ( token . TokenType != TSqlTokenType . WhiteSpace )
142+ {
143+ // Stop at next non-whitespace, non-comment token
144+ break ;
145+ }
145146 }
146147 }
147148
148149 /// <summary>
149- /// Emits a single-line comment .
150+ /// Updates tracking after generating a fragment .
150151 /// </summary>
151- private void EmitSingleLineComment ( string text , CommentPosition position )
152+ /// <param name="fragment">The fragment that was just generated.</param>
153+ protected void UpdateLastProcessedIndex ( TSqlFragment fragment )
152154 {
153- if ( position == CommentPosition . Trailing )
155+ if ( fragment != null && fragment . LastTokenIndex > _lastProcessedTokenIndex )
154156 {
155- // Trailing: add space before comment, keep on same line
156- _writer . AddToken ( ScriptGeneratorSupporter . CreateWhitespaceToken ( 1 ) ) ;
157+ _lastProcessedTokenIndex = fragment . LastTokenIndex ;
157158 }
158- else
159+ }
160+
161+ /// <summary>
162+ /// Called from GenerateFragmentIfNotNull to handle comments before generating a fragment.
163+ /// This is the key integration point that enables comments within sub-expressions.
164+ /// </summary>
165+ /// <param name="fragment">The fragment about to be generated.</param>
166+ protected void BeforeGenerateFragment ( TSqlFragment fragment )
167+ {
168+ if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
159169 {
160- // Leading: comment goes on its own line
161- // Indentation is already applied by current context
170+ return ;
162171 }
163172
164- // Write the comment as-is (preserving the -- prefix)
165- _writer . AddToken ( new TSqlParserToken ( TSqlTokenType . SingleLineComment , text ) ) ;
166-
167- if ( position == CommentPosition . Leading )
173+ // Emit file-level leading comments once
174+ if ( ! _leadingCommentsEmitted )
168175 {
169- // After a leading comment, we need a newline
170- _writer . NewLine ( ) ;
176+ EmitLeadingComments ( fragment ) ;
177+ _leadingCommentsEmitted = true ;
171178 }
179+
180+ // Emit any comments in the gap between last processed token and this fragment
181+ EmitGapComments ( fragment ) ;
172182 }
173183
174184 /// <summary>
175- /// Emits a multi-line comment, preserving internal structure .
185+ /// Called from GenerateFragmentIfNotNull to handle comments after generating a fragment .
176186 /// </summary>
177- private void EmitMultiLineComment ( string text , CommentPosition position )
187+ /// <param name="fragment">The fragment that was just generated.</param>
188+ protected void AfterGenerateFragment ( TSqlFragment fragment )
178189 {
179- if ( position == CommentPosition . Trailing )
190+ if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
180191 {
181- // Trailing: add space before comment
182- _writer . AddToken ( ScriptGeneratorSupporter . CreateWhitespaceToken ( 1 ) ) ;
192+ return ;
183193 }
184194
185- // For multi-line comments, we preserve the content as-is
186- // The comment includes /* and */ delimiters
187- _writer . AddToken ( new TSqlParserToken ( TSqlTokenType . MultilineComment , text ) ) ;
188-
189- if ( position == CommentPosition . Leading )
190- {
191- // After a leading multi-line comment, add newline
192- _writer . NewLine ( ) ;
193- }
195+ // Emit trailing comments and update tracking
196+ EmitTrailingComments ( fragment ) ;
197+ UpdateLastProcessedIndex ( fragment ) ;
194198 }
195199
196200 /// <summary>
197- /// Called before visiting a fragment to emit any leading comments .
201+ /// Emits a comment token to the output .
198202 /// </summary>
199- /// <param name="fragment">The fragment about to be visited.</param>
200- protected void BeforeVisitFragment ( TSqlFragment fragment )
203+ /// <param name="token">The comment token.</param>
204+ /// <param name="isLeading">True if this is a leading comment, false for trailing.</param>
205+ private void EmitCommentToken ( TSqlParserToken token , bool isLeading )
201206 {
202- if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
207+ if ( token == null )
203208 {
204209 return ;
205210 }
206211
207- var leadingComments = GetLeadingComments ( fragment ) ;
208- foreach ( var comment in leadingComments )
212+ if ( token . TokenType == TSqlTokenType . SingleLineComment )
213+ {
214+ if ( ! isLeading )
215+ {
216+ // Trailing: add space before comment
217+ _writer . AddToken ( ScriptGeneratorSupporter . CreateWhitespaceToken ( 1 ) ) ;
218+ }
219+
220+ _writer . AddToken ( new TSqlParserToken ( TSqlTokenType . SingleLineComment , token . Text ) ) ;
221+
222+ if ( isLeading )
223+ {
224+ // After a leading comment, add newline
225+ _writer . NewLine ( ) ;
226+ }
227+ }
228+ else if ( token . TokenType == TSqlTokenType . MultilineComment )
209229 {
210- EmitComment ( comment ) ;
230+ if ( ! isLeading )
231+ {
232+ // Trailing: add space before comment
233+ _writer . AddToken ( ScriptGeneratorSupporter . CreateWhitespaceToken ( 1 ) ) ;
234+ }
235+
236+ _writer . AddToken ( new TSqlParserToken ( TSqlTokenType . MultilineComment , token . Text ) ) ;
237+
238+ if ( isLeading )
239+ {
240+ // After a leading multi-line comment, add newline
241+ _writer . NewLine ( ) ;
242+ }
211243 }
212244 }
213245
214246 /// <summary>
215- /// Called after visiting a fragment to emit any trailing comments and update tracking.
247+ /// Emits any remaining comments at the end of the token stream.
248+ /// Call this after visiting the root fragment to capture comments that appear
249+ /// after the last statement (end-of-script comments).
216250 /// </summary>
217- /// <param name="fragment">The fragment that was just visited.</param>
218- protected void AfterVisitFragment ( TSqlFragment fragment )
251+ protected void EmitRemainingComments ( )
219252 {
220- if ( ! _options . PreserveComments || _currentTokenStream == null || fragment == null )
253+ if ( ! _options . PreserveComments || _currentTokenStream == null )
221254 {
222255 return ;
223256 }
224257
225- var trailingComments = GetTrailingComments ( fragment ) ;
226- foreach ( var comment in trailingComments )
258+ // Scan from the last processed token to the end of the token stream
259+ for ( int i = _lastProcessedTokenIndex + 1 ; i < _currentTokenStream . Count ; i ++ )
227260 {
228- EmitComment ( comment ) ;
229- }
230-
231- // Update the last processed token index
232- if ( fragment . LastTokenIndex >= 0 )
233- {
234- // Account for any trailing comments we just emitted
235- int newLastIndex = fragment . LastTokenIndex ;
236- foreach ( var comment in trailingComments )
261+ var token = _currentTokenStream [ i ] ;
262+ if ( IsCommentToken ( token ) && ! _emittedComments . Contains ( token ) )
237263 {
238- // Find the index of this comment token
239- for ( int i = newLastIndex + 1 ; i < _currentTokenStream . Count ; i ++ )
240- {
241- if ( _currentTokenStream [ i ] == comment . Token )
242- {
243- newLastIndex = i ;
244- break ;
245- }
246- }
264+ // End-of-script comments: add newline before, emit comment
265+ _writer . NewLine ( ) ;
266+ _writer . AddToken ( new TSqlParserToken ( token . TokenType , token . Text ) ) ;
267+ _emittedComments . Add ( token ) ;
247268 }
248- _lastProcessedTokenIndex = newLastIndex ;
249269 }
250270 }
251271
0 commit comments