-
Notifications
You must be signed in to change notification settings - Fork 37
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
Open
dzsquared
wants to merge
5
commits into
main
Choose a base branch
from
dev/drskwier-commentsv2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
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
There are no files selected for viewing
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; | ||
|
|
||
| /// <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
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
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
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,9 @@ | ||
| SELECT 1; | ||
|
|
||
| SELECT 2; | ||
|
|
||
| SELECT 3; | ||
|
|
||
| SELECT 4; | ||
|
|
||
| SELECT 5; |
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,10 @@ | ||
| SELECT 1; | ||
|
|
||
| SELECT * | ||
| FROM dbo.MyTable; | ||
|
|
||
| SELECT 2; | ||
|
|
||
| SELECT 3; | ||
|
|
||
| SELECT 4; |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _lastProcessedTokenIndex and _currentTokenStream fields are instance fields of SqlScriptGeneratorVisitor without any thread synchronization. If the same SqlScriptGenerator instance is used concurrently from multiple threads (which is not a typical pattern but is possible), these fields could be corrupted. Consider documenting that SqlScriptGenerator instances are not thread-safe and should not be shared across threads, or add appropriate thread-safety mechanisms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@llali curious your thoughts on thread safety for scriptgenerator. The overall overhead of a shared vs individual instances of the script generator across threads for common scenarios seems minimal so the justification doesn't seem to be there, but I'm potentially missing something.