Skip to content

Commit e134349

Browse files
committed
hybridized approach with v1
1 parent db61757 commit e134349

5 files changed

Lines changed: 666 additions & 139 deletions

File tree

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

Lines changed: 142 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -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

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,19 @@ partial class SqlScriptGeneratorVisitor
1111
{
1212
public override void ExplicitVisit(TSqlBatch node)
1313
{
14-
// Emit leading comments before the batch
15-
BeforeVisitFragment(node);
16-
14+
// Comments are now handled automatically by GenerateFragmentIfNotNull
1715
foreach (TSqlStatement statement in node.Statements)
1816
{
19-
// Emit leading comments before each statement
20-
BeforeVisitFragment(statement);
21-
2217
GenerateFragmentIfNotNull(statement);
2318

2419
GenerateSemiColonWhenNecessary(statement);
2520

26-
// Emit trailing comments after each statement
27-
AfterVisitFragment(statement);
28-
2921
if (statement is TSqlStatementSnippet == false)
3022
{
3123
NewLine();
3224
NewLine();
3325
}
3426
}
35-
36-
// Emit trailing comments after the batch
37-
AfterVisitFragment(node);
3827
}
3928
}
4029
}

0 commit comments

Comments
 (0)