Skip to content

Commit 6783258

Browse files
committed
revised approach
1 parent c152271 commit 6783258

12 files changed

Lines changed: 604 additions & 1 deletion
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//------------------------------------------------------------------------------
2+
// <copyright file="CommentInfo.cs" company="Microsoft">
3+
// Copyright (c) Microsoft Corporation. All rights reserved.
4+
// </copyright>
5+
//------------------------------------------------------------------------------
6+
7+
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
8+
{
9+
/// <summary>
10+
/// Intermediate structure for processing comments during script generation.
11+
/// </summary>
12+
internal sealed class CommentInfo
13+
{
14+
/// <summary>
15+
/// Gets or sets the original comment token from the token stream.
16+
/// </summary>
17+
public TSqlParserToken Token { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the position of the comment relative to code.
21+
/// </summary>
22+
public CommentPosition Position { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the token index of the associated fragment.
26+
/// </summary>
27+
public int AssociatedFragmentIndex { get; set; }
28+
29+
/// <summary>
30+
/// Gets whether this is a single-line comment (-- style).
31+
/// </summary>
32+
public bool IsSingleLineComment
33+
{
34+
get { return Token != null && Token.TokenType == TSqlTokenType.SingleLineComment; }
35+
}
36+
37+
/// <summary>
38+
/// Gets whether this is a multi-line comment (/* */ style).
39+
/// </summary>
40+
public bool IsMultiLineComment
41+
{
42+
get { return Token != null && Token.TokenType == TSqlTokenType.MultilineComment; }
43+
}
44+
45+
/// <summary>
46+
/// Gets the text content of the comment.
47+
/// </summary>
48+
public string Text
49+
{
50+
get { return Token?.Text; }
51+
}
52+
53+
/// <summary>
54+
/// Creates a new CommentInfo instance.
55+
/// </summary>
56+
/// <param name="token">The comment token.</param>
57+
/// <param name="position">The position relative to code.</param>
58+
/// <param name="fragmentIndex">The associated fragment token index.</param>
59+
public CommentInfo(TSqlParserToken token, CommentPosition position, int fragmentIndex)
60+
{
61+
Token = token;
62+
Position = position;
63+
AssociatedFragmentIndex = fragmentIndex;
64+
}
65+
}
66+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//------------------------------------------------------------------------------
2+
// <copyright file="CommentPosition.cs" company="Microsoft">
3+
// Copyright (c) Microsoft Corporation. All rights reserved.
4+
// </copyright>
5+
//------------------------------------------------------------------------------
6+
7+
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
8+
{
9+
/// <summary>
10+
/// Categorizes comment placement relative to code during script generation.
11+
/// </summary>
12+
internal enum CommentPosition
13+
{
14+
/// <summary>
15+
/// Comment appears before the associated code on previous line(s).
16+
/// </summary>
17+
Leading,
18+
19+
/// <summary>
20+
/// Comment appears after code on the same line.
21+
/// </summary>
22+
Trailing,
23+
24+
/// <summary>
25+
/// Comment not directly associated with code (e.g., end of file, standalone block).
26+
/// </summary>
27+
Standalone
28+
}
29+
}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
//------------------------------------------------------------------------------
2+
// <copyright file="SqlScriptGeneratorVisitor.Comments.cs" company="Microsoft">
3+
// Copyright (c) Microsoft Corporation. All rights reserved.
4+
// </copyright>
5+
//------------------------------------------------------------------------------
6+
using System;
7+
using System.Collections.Generic;
8+
9+
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
10+
{
11+
internal abstract partial class SqlScriptGeneratorVisitor
12+
{
13+
#region Comment Tracking Fields
14+
15+
/// <summary>
16+
/// Tracks the last token index processed for comment emission.
17+
/// Used to find comments between visited fragments.
18+
/// </summary>
19+
private int _lastProcessedTokenIndex = -1;
20+
21+
/// <summary>
22+
/// The current script's token stream, set when visiting begins.
23+
/// </summary>
24+
private IList<TSqlParserToken> _currentTokenStream;
25+
26+
#endregion
27+
28+
#region Comment Preservation Methods
29+
30+
/// <summary>
31+
/// Sets the token stream for comment tracking.
32+
/// Call this before visiting the root node when PreserveComments is enabled.
33+
/// </summary>
34+
/// <param name="tokenStream">The token stream from the parsed script.</param>
35+
protected void SetTokenStreamForComments(IList<TSqlParserToken> tokenStream)
36+
{
37+
_currentTokenStream = tokenStream;
38+
_lastProcessedTokenIndex = -1;
39+
}
40+
41+
/// <summary>
42+
/// Gets leading comments that appear between the last processed token and the current fragment.
43+
/// </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)
47+
{
48+
var comments = new List<CommentInfo>();
49+
50+
if (_currentTokenStream == null || fragment == null || !_options.PreserveComments)
51+
{
52+
return comments;
53+
}
54+
55+
int startIndex = _lastProcessedTokenIndex + 1;
56+
int endIndex = fragment.FirstTokenIndex;
57+
58+
// Scan for comments between last processed and current fragment
59+
for (int i = startIndex; i < endIndex && i < _currentTokenStream.Count; i++)
60+
{
61+
var token = _currentTokenStream[i];
62+
if (IsCommentToken(token))
63+
{
64+
comments.Add(new CommentInfo(token, CommentPosition.Leading, fragment.FirstTokenIndex));
65+
}
66+
}
67+
68+
return comments;
69+
}
70+
71+
/// <summary>
72+
/// Gets trailing comments that appear on the same line after the fragment.
73+
/// </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)
77+
{
78+
var comments = new List<CommentInfo>();
79+
80+
if (_currentTokenStream == null || fragment == null || !_options.PreserveComments)
81+
{
82+
return comments;
83+
}
84+
85+
int lastTokenIndex = fragment.LastTokenIndex;
86+
if (lastTokenIndex < 0 || lastTokenIndex >= _currentTokenStream.Count)
87+
{
88+
return comments;
89+
}
90+
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++)
96+
{
97+
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))
107+
{
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+
}
115+
}
116+
}
117+
118+
return comments;
119+
}
120+
121+
/// <summary>
122+
/// Emits a comment using the script writer with current indentation.
123+
/// </summary>
124+
/// <param name="commentInfo">The comment to emit.</param>
125+
protected void EmitComment(CommentInfo commentInfo)
126+
{
127+
if (commentInfo == null || commentInfo.Token == null)
128+
{
129+
return;
130+
}
131+
132+
var text = commentInfo.Text;
133+
if (string.IsNullOrEmpty(text))
134+
{
135+
return;
136+
}
137+
138+
if (commentInfo.IsSingleLineComment)
139+
{
140+
EmitSingleLineComment(text, commentInfo.Position);
141+
}
142+
else if (commentInfo.IsMultiLineComment)
143+
{
144+
EmitMultiLineComment(text, commentInfo.Position);
145+
}
146+
}
147+
148+
/// <summary>
149+
/// Emits a single-line comment.
150+
/// </summary>
151+
private void EmitSingleLineComment(string text, CommentPosition position)
152+
{
153+
if (position == CommentPosition.Trailing)
154+
{
155+
// Trailing: add space before comment, keep on same line
156+
_writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1));
157+
}
158+
else
159+
{
160+
// Leading: comment goes on its own line
161+
// Indentation is already applied by current context
162+
}
163+
164+
// Write the comment as-is (preserving the -- prefix)
165+
_writer.AddToken(new TSqlParserToken(TSqlTokenType.SingleLineComment, text));
166+
167+
if (position == CommentPosition.Leading)
168+
{
169+
// After a leading comment, we need a newline
170+
_writer.NewLine();
171+
}
172+
}
173+
174+
/// <summary>
175+
/// Emits a multi-line comment, preserving internal structure.
176+
/// </summary>
177+
private void EmitMultiLineComment(string text, CommentPosition position)
178+
{
179+
if (position == CommentPosition.Trailing)
180+
{
181+
// Trailing: add space before comment
182+
_writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1));
183+
}
184+
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+
}
194+
}
195+
196+
/// <summary>
197+
/// Called before visiting a fragment to emit any leading comments.
198+
/// </summary>
199+
/// <param name="fragment">The fragment about to be visited.</param>
200+
protected void BeforeVisitFragment(TSqlFragment fragment)
201+
{
202+
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
203+
{
204+
return;
205+
}
206+
207+
var leadingComments = GetLeadingComments(fragment);
208+
foreach (var comment in leadingComments)
209+
{
210+
EmitComment(comment);
211+
}
212+
}
213+
214+
/// <summary>
215+
/// Called after visiting a fragment to emit any trailing comments and update tracking.
216+
/// </summary>
217+
/// <param name="fragment">The fragment that was just visited.</param>
218+
protected void AfterVisitFragment(TSqlFragment fragment)
219+
{
220+
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
221+
{
222+
return;
223+
}
224+
225+
var trailingComments = GetTrailingComments(fragment);
226+
foreach (var comment in trailingComments)
227+
{
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)
237+
{
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+
}
247+
}
248+
_lastProcessedTokenIndex = newLastIndex;
249+
}
250+
}
251+
252+
/// <summary>
253+
/// Checks if a token is a comment token.
254+
/// </summary>
255+
private static bool IsCommentToken(TSqlParserToken token)
256+
{
257+
return token != null &&
258+
(token.TokenType == TSqlTokenType.SingleLineComment ||
259+
token.TokenType == TSqlTokenType.MultilineComment);
260+
}
261+
262+
#endregion
263+
}
264+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,30 @@ partial class SqlScriptGeneratorVisitor
1111
{
1212
public override void ExplicitVisit(TSqlBatch node)
1313
{
14+
// Emit leading comments before the batch
15+
BeforeVisitFragment(node);
16+
1417
foreach (TSqlStatement statement in node.Statements)
1518
{
19+
// Emit leading comments before each statement
20+
BeforeVisitFragment(statement);
21+
1622
GenerateFragmentIfNotNull(statement);
1723

1824
GenerateSemiColonWhenNecessary(statement);
1925

26+
// Emit trailing comments after each statement
27+
AfterVisitFragment(statement);
28+
2029
if (statement is TSqlStatementSnippet == false)
2130
{
2231
NewLine();
2332
NewLine();
2433
}
2534
}
35+
36+
// Emit trailing comments after the batch
37+
AfterVisitFragment(node);
2638
}
2739
}
2840
}

0 commit comments

Comments
 (0)