// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Diagnostics; using System.Globalization; using System.Linq; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom; using Microsoft.SqlTools.ServiceLayer.Extensibility; namespace Microsoft.SqlTools.ServiceLayer.Formatter { /// /// The main entry point for our formatter implementation, via the method. /// This converts a text string into a parsed AST using the Intellisense parser. /// It then uses the Visitor pattern to find each element in the tree and determine if any edits are needed based on /// All edits are applied after the entire AST has been visited using an algorithm that keeps track of index changes caused by previous updates. This allows /// us to apply multiple edits to a text string in one sweep. /// /// A note on the implementation: All of the override nodes in the Intellisense AST are defined here, and routed to the Format method which looks up a matching /// formatter to handle them. Any entry not explicitly formatted will use the no-op formatter which passes through the text unchanged. /// internal partial class FormatterVisitor : SqlCodeObjectVisitor { private readonly IMultiServiceProvider serviceProvider; public FormatterVisitor(FormatContext context, IMultiServiceProvider serviceProvider) : base() { Context = context; this.serviceProvider = serviceProvider; } private void Format(T codeObject) where T : SqlCodeObject { ASTNodeFormatter f = GetFormatter(codeObject); f.Format(); } private ASTNodeFormatter GetFormatter(T codeObject) where T:SqlCodeObject { Type astType = typeof(T); ASTNodeFormatter formatter; var formatterFactory = serviceProvider.GetServices().FirstOrDefault(f => astType.Equals(f.SupportedNodeType)); if (formatterFactory != null) { formatter = formatterFactory.Create(this, codeObject); } else { formatter = new NoOpFormatter(this, codeObject); } return formatter; } public FormatContext Context { get; private set; } public void VerifyFormat() { ParseResult result = Parser.Parse(Context.FormattedSql); SqlScript newScript = result.Script; VerifyTokenStreamsOnlyDifferByWhitespace(Context.Script, newScript); } internal static bool IsTokenWhitespaceOrComma(SqlScript script, int tokenIndex) { int tokenId = script.TokenManager.TokenList[tokenIndex].TokenId; return script.TokenManager.IsTokenWhitespace(tokenId) || (tokenId == 44); } internal static bool IsTokenWhitespaceOrComment(SqlScript script, int tokenIndex) { int tokenId = script.TokenManager.TokenList[tokenIndex].TokenId; return script.TokenManager.IsTokenWhitespace(tokenId) || script.TokenManager.IsTokenComment(tokenId); } /// /// Checks that the token streams of two SqlScript objects differ only by whitespace tokens or /// by the relative positioning of commas and comments. The important rule enforced is that there are /// no changes in relative positioning which involve tokens other than commas, comments or whitespaces. /// /// SQL script containing the first token stream. /// SQL script containing the second token stream. public static void VerifyTokenStreamsOnlyDifferByWhitespace(SqlScript script1, SqlScript script2) { // We break down the relative positioning problem into assuring that the token streams have identical ids // both when we ignore whitespaces and commas as well as when we ignore whitespaces and comments VerifyTokenStreamsOnlyDifferBy(script1, script2, IsTokenWhitespaceOrComma); VerifyTokenStreamsOnlyDifferBy(script1, script2, IsTokenWhitespaceOrComment); } internal delegate bool IgnoreToken(SqlScript script, int tokenIndex); public static void VerifyTokenStreamsOnlyDifferBy(SqlScript script1, SqlScript script2, IgnoreToken ignoreToken ) { int t1 = 0; int t2 = 0; while (t1 < script1.TokenManager.Count && t2 < script2.TokenManager.Count) { // advance t1 until it is pointing at a non-whitespace token while (t1 < script1.TokenManager.Count && ignoreToken(script1, t1)) { ++t1; } // advance t2 until it is pointing at a non-whitespace token while (t2 < script2.TokenManager.Count && ignoreToken(script2, t2)) { ++t2; } if (t1 >= script1.TokenManager.Count || t2 >= script2.TokenManager.Count) { break; } // // TODO: need special logic here to deal with the placement of commas // // verify the tokens are equal if (script1.TokenManager.TokenList[t1].TokenId != script2.TokenManager.TokenList[t2].TokenId) { string msg = "The comparison failed between tokens at {0} & {1}. The token IDs were {2} and {3} respectively. Script1 = {4}. Script2 = {5}"; msg = string.Format(CultureInfo.CurrentCulture, msg, t1, t2, script1.TokenManager.TokenList[t1].TokenId, script2.TokenManager.TokenList[t2].TokenId, script1.Sql, script2.Sql); throw new FormatFailedException(msg); } ++t1; ++t2; } // one of the streams is exhausted, verify that the only tokens left in the other stream are whitespace tokens Debug.Assert(t1 >= script1.TokenManager.Count || t2 >= script2.TokenManager.Count, "expected to be at the end of one of the token's streams"); int t = t1; SqlScript s = script1; if (t2 < script2.TokenManager.Count) { Debug.Assert(t1 >= script1.TokenManager.Count, "expected to be at end of script1's token stream"); t = t2; s = script2; } while (t < s.TokenManager.Count) { if (!ignoreToken(s, t)) { string msg = "Unexpected non-whitespace token at index {0}, token ID {1}"; msg = string.Format(CultureInfo.CurrentCulture, msg, t, s.TokenManager.TokenList[t].TokenId); throw new FormatFailedException(msg); } } } } }