-
Notifications
You must be signed in to change notification settings - Fork 38
adds formatter option for preserving comments (redo) #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6783258
revised approach
dzsquared db61757
expanded tests
dzsquared e134349
hybridized approach with v1
dzsquared 6c81cbb
cleanup unused code
dzsquared b798359
excessive comment cleanup
dzsquared a823e73
improving method names
dzsquared File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/CommentInfo.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| //------------------------------------------------------------------------------ | ||
| // <copyright file="CommentInfo.cs" company="Microsoft"> | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // </copyright> | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator | ||
| { | ||
| /// <summary> | ||
| /// Intermediate structure for processing comments during script generation. | ||
| /// </summary> | ||
| internal sealed class CommentInfo | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the original comment token from the token stream. | ||
| /// </summary> | ||
| public TSqlParserToken Token { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the position of the comment relative to code. | ||
| /// </summary> | ||
| public CommentPosition Position { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the token index of the associated fragment. | ||
| /// </summary> | ||
| public int AssociatedFragmentIndex { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether this is a single-line comment (-- style). | ||
| /// </summary> | ||
| public bool IsSingleLineComment | ||
| { | ||
| get { return Token != null && Token.TokenType == TSqlTokenType.SingleLineComment; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether this is a multi-line comment (/* */ style). | ||
| /// </summary> | ||
| public bool IsMultiLineComment | ||
| { | ||
| get { return Token != null && Token.TokenType == TSqlTokenType.MultilineComment; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the text content of the comment. | ||
| /// </summary> | ||
| public string Text | ||
| { | ||
| get { return Token?.Text; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new CommentInfo instance. | ||
| /// </summary> | ||
| /// <param name="token">The comment token.</param> | ||
| /// <param name="position">The position relative to code.</param> | ||
| /// <param name="fragmentIndex">The associated fragment token index.</param> | ||
| public CommentInfo(TSqlParserToken token, CommentPosition position, int fragmentIndex) | ||
| { | ||
| Token = token; | ||
| Position = position; | ||
| AssociatedFragmentIndex = fragmentIndex; | ||
| } | ||
| } | ||
| } | ||
29 changes: 29 additions & 0 deletions
29
SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/CommentPosition.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| //------------------------------------------------------------------------------ | ||
| // <copyright file="CommentPosition.cs" company="Microsoft"> | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // </copyright> | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator | ||
| { | ||
| /// <summary> | ||
| /// Categorizes comment placement relative to code during script generation. | ||
| /// </summary> | ||
| internal enum CommentPosition | ||
| { | ||
| /// <summary> | ||
| /// Comment appears before the associated code on previous line(s). | ||
| /// </summary> | ||
| Leading, | ||
|
|
||
| /// <summary> | ||
| /// Comment appears after code on the same line. | ||
| /// </summary> | ||
| Trailing, | ||
|
|
||
| /// <summary> | ||
| /// Comment not directly associated with code (e.g., end of file, standalone block). | ||
| /// </summary> | ||
| Standalone | ||
dzsquared marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
dzsquared marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
284 changes: 284 additions & 0 deletions
284
SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,284 @@ | ||
| //------------------------------------------------------------------------------ | ||
| // <copyright file="SqlScriptGeneratorVisitor.Comments.cs" company="Microsoft"> | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // </copyright> | ||
| //------------------------------------------------------------------------------ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator | ||
| { | ||
| internal abstract partial class SqlScriptGeneratorVisitor | ||
| { | ||
| #region Comment Tracking Fields | ||
|
|
||
| /// <summary> | ||
| /// Tracks the last token index processed for comment emission. | ||
| /// Used to find comments between visited fragments. | ||
| /// </summary> | ||
| private int _lastProcessedTokenIndex = -1; | ||
|
|
||
| /// <summary> | ||
| /// The current script's token stream, set when visiting begins. | ||
| /// </summary> | ||
| private IList<TSqlParserToken> _currentTokenStream; | ||
dzsquared marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <summary> | ||
| /// Tracks which comment tokens have already been emitted to avoid duplicates. | ||
| /// </summary> | ||
| private readonly HashSet<TSqlParserToken> _emittedComments = new HashSet<TSqlParserToken>(); | ||
|
|
||
| /// <summary> | ||
| /// Tracks whether leading (file-level) comments have been emitted. | ||
| /// </summary> | ||
| private bool _leadingCommentsEmitted = false; | ||
|
|
||
| #endregion | ||
|
|
||
| #region Comment Preservation Methods | ||
|
|
||
| /// <summary> | ||
| /// Sets the token stream for comment tracking. | ||
| /// Call this before visiting the root node when PreserveComments is enabled. | ||
| /// </summary> | ||
| /// <param name="tokenStream">The token stream from the parsed script.</param> | ||
| protected void SetTokenStreamForComments(IList<TSqlParserToken> tokenStream) | ||
| { | ||
| _currentTokenStream = tokenStream; | ||
| _lastProcessedTokenIndex = -1; | ||
| _emittedComments.Clear(); | ||
| _leadingCommentsEmitted = false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits comments that appear before the first fragment in the script (file-level leading comments). | ||
| /// Called once when generating the first fragment. | ||
| /// </summary> | ||
| /// <param name="fragment">The first fragment being generated.</param> | ||
| protected void EmitLeadingComments(TSqlFragment fragment) | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null || fragment == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (fragment.FirstTokenIndex <= 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| for (int i = 0; i < fragment.FirstTokenIndex && i < _currentTokenStream.Count; i++) | ||
| { | ||
| var token = _currentTokenStream[i]; | ||
| if (IsCommentToken(token) && !_emittedComments.Contains(token)) | ||
| { | ||
| EmitCommentToken(token, isLeading: true); | ||
| _emittedComments.Add(token); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits comments that appear in the gap between the last emitted token and the current fragment. | ||
| /// This captures comments embedded within sub-expressions. | ||
| /// </summary> | ||
| /// <param name="fragment">The fragment about to be generated.</param> | ||
| protected void EmitGapComments(TSqlFragment fragment) | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null || fragment == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| int startIndex = _lastProcessedTokenIndex + 1; | ||
| int endIndex = fragment.FirstTokenIndex; | ||
|
|
||
| if (endIndex <= startIndex) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| for (int i = startIndex; i < endIndex && i < _currentTokenStream.Count; i++) | ||
| { | ||
| var token = _currentTokenStream[i]; | ||
| if (IsCommentToken(token) && !_emittedComments.Contains(token)) | ||
| { | ||
| EmitCommentToken(token, isLeading: true); | ||
| _emittedComments.Add(token); | ||
| _lastProcessedTokenIndex = i; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits trailing comments that appear immediately after the fragment. | ||
| /// </summary> | ||
| /// <param name="fragment">The fragment that was just generated.</param> | ||
| protected void EmitTrailingComments(TSqlFragment fragment) | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null || fragment == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| int lastTokenIndex = fragment.LastTokenIndex; | ||
| if (lastTokenIndex < 0 || lastTokenIndex >= _currentTokenStream.Count) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Scan for comments immediately following the fragment | ||
| for (int i = lastTokenIndex + 1; i < _currentTokenStream.Count; i++) | ||
| { | ||
| var token = _currentTokenStream[i]; | ||
|
|
||
| if (IsCommentToken(token) && !_emittedComments.Contains(token)) | ||
| { | ||
| EmitCommentToken(token, isLeading: false); | ||
| _emittedComments.Add(token); | ||
| _lastProcessedTokenIndex = i; | ||
| } | ||
| else if (token.TokenType != TSqlTokenType.WhiteSpace) | ||
| { | ||
| // Stop at next non-whitespace, non-comment token | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Updates tracking after generating a fragment. | ||
| /// </summary> | ||
| /// <param name="fragment">The fragment that was just generated.</param> | ||
| protected void UpdateLastProcessedIndex(TSqlFragment fragment) | ||
| { | ||
| if (fragment != null && fragment.LastTokenIndex > _lastProcessedTokenIndex) | ||
| { | ||
| _lastProcessedTokenIndex = fragment.LastTokenIndex; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Called from GenerateFragmentIfNotNull to handle comments before generating a fragment. | ||
| /// This is the key integration point that enables comments within sub-expressions. | ||
| /// </summary> | ||
| /// <param name="fragment">The fragment about to be generated.</param> | ||
| protected void BeforeGenerateFragment(TSqlFragment fragment) | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null || fragment == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Emit file-level leading comments once | ||
| if (!_leadingCommentsEmitted) | ||
| { | ||
| EmitLeadingComments(fragment); | ||
| _leadingCommentsEmitted = true; | ||
| } | ||
|
|
||
| // Emit any comments in the gap between last processed token and this fragment | ||
| EmitGapComments(fragment); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Called from GenerateFragmentIfNotNull to handle comments after generating a fragment. | ||
| /// </summary> | ||
| /// <param name="fragment">The fragment that was just generated.</param> | ||
| protected void AfterGenerateFragment(TSqlFragment fragment) | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null || fragment == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Emit trailing comments and update tracking | ||
| EmitTrailingComments(fragment); | ||
| UpdateLastProcessedIndex(fragment); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits a comment token to the output. | ||
| /// </summary> | ||
| /// <param name="token">The comment token.</param> | ||
| /// <param name="isLeading">True if this is a leading comment, false for trailing.</param> | ||
| private void EmitCommentToken(TSqlParserToken token, bool isLeading) | ||
| { | ||
| if (token == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (token.TokenType == TSqlTokenType.SingleLineComment) | ||
| { | ||
| if (!isLeading) | ||
| { | ||
| // Trailing: add space before comment | ||
| _writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); | ||
| } | ||
|
|
||
| _writer.AddToken(new TSqlParserToken(TSqlTokenType.SingleLineComment, token.Text)); | ||
|
|
||
| if (isLeading) | ||
| { | ||
| // After a leading comment, add newline | ||
| _writer.NewLine(); | ||
| } | ||
| } | ||
| else if (token.TokenType == TSqlTokenType.MultilineComment) | ||
| { | ||
| if (!isLeading) | ||
| { | ||
| // Trailing: add space before comment | ||
| _writer.AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); | ||
| } | ||
|
|
||
| _writer.AddToken(new TSqlParserToken(TSqlTokenType.MultilineComment, token.Text)); | ||
|
|
||
| if (isLeading) | ||
| { | ||
| // After a leading multi-line comment, add newline | ||
| _writer.NewLine(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Emits any remaining comments at the end of the token stream. | ||
| /// Call this after visiting the root fragment to capture comments that appear | ||
| /// after the last statement (end-of-script comments). | ||
| /// </summary> | ||
| protected void EmitRemainingComments() | ||
| { | ||
| if (!_options.PreserveComments || _currentTokenStream == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Scan from the last processed token to the end of the token stream | ||
| for (int i = _lastProcessedTokenIndex + 1; i < _currentTokenStream.Count; i++) | ||
| { | ||
| var token = _currentTokenStream[i]; | ||
| if (IsCommentToken(token) && !_emittedComments.Contains(token)) | ||
| { | ||
| // End-of-script comments: add newline before, emit comment | ||
| _writer.NewLine(); | ||
| _writer.AddToken(new TSqlParserToken(token.TokenType, token.Text)); | ||
| _emittedComments.Add(token); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if a token is a comment token. | ||
| /// </summary> | ||
| private static bool IsCommentToken(TSqlParserToken token) | ||
| { | ||
| return token != null && | ||
| (token.TokenType == TSqlTokenType.SingleLineComment || | ||
| token.TokenType == TSqlTokenType.MultilineComment); | ||
| } | ||
|
|
||
| #endregion | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.