//
// 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);
}
}
}
}
}