Skip to content

Commit 265ba33

Browse files
authored
Place semicolon before inline comment (#197)
1 parent d503232 commit 265ba33

7 files changed

+167
-11
lines changed

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ internal abstract partial class SqlScriptGeneratorVisitor
3333
/// </summary>
3434
private bool _leadingCommentsEmitted = false;
3535

36+
/// <summary>
37+
/// When true, suppresses trailing comment emission in HandleCommentsAfterFragment
38+
/// for fragments whose LastTokenIndex matches or exceeds _suppressTrailingCommentsAfterIndex.
39+
/// Used by GenerateStatementWithSemiColon to defer trailing comments until after
40+
/// the semicolon has been placed, without affecting inter-clause comments.
41+
/// </summary>
42+
private bool _suppressTrailingComments = false;
43+
44+
/// <summary>
45+
/// The LastTokenIndex of the statement for which trailing comments are being suppressed.
46+
/// Only comments after this index are suppressed.
47+
/// </summary>
48+
private int _suppressTrailingCommentsAfterIndex = -1;
49+
3650
#endregion
3751

3852
#region Comment Preservation Methods
@@ -48,6 +62,8 @@ protected void SetTokenStreamForComments(IList<TSqlParserToken> tokenStream)
4862
_lastProcessedTokenIndex = -1;
4963
_emittedComments.Clear();
5064
_leadingCommentsEmitted = false;
65+
_suppressTrailingComments = false;
66+
_suppressTrailingCommentsAfterIndex = -1;
5167
}
5268

5369
/// <summary>
@@ -192,6 +208,16 @@ protected void HandleCommentsAfterFragment(TSqlFragment fragment)
192208
return;
193209
}
194210

211+
// When trailing comments are suppressed (e.g., during statement body generation
212+
// before semicolon placement), skip emitting trailing comments only for fragments
213+
// whose last token is at or past the statement boundary. Inter-clause comments
214+
// (within the statement) are still emitted normally.
215+
if (_suppressTrailingComments && fragment.LastTokenIndex >= _suppressTrailingCommentsAfterIndex)
216+
{
217+
UpdateLastProcessedIndex(fragment);
218+
return;
219+
}
220+
195221
// Emit trailing comments and update tracking
196222
EmitTrailingComments(fragment);
197223
UpdateLastProcessedIndex(fragment);

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,46 @@ protected void GenerateSemiColonWhenNecessary(TSqlStatement node)
440440
}
441441
}
442442

443+
/// <summary>
444+
/// Generates a statement fragment with semicolon placed before any trailing comments.
445+
/// This prevents semicolons from being appended after single-line comments (-- style),
446+
/// which would make them part of the comment text.
447+
/// </summary>
448+
protected void GenerateStatementWithSemiColon(TSqlStatement statement)
449+
{
450+
if (statement == null)
451+
{
452+
return;
453+
}
454+
455+
// Handle comments before
456+
HandleCommentsBeforeFragment(statement);
457+
458+
// Suppress trailing comment emission during statement body generation
459+
// so that the semicolon can be placed before trailing comments.
460+
// Only suppress for fragments at the statement boundary (LastTokenIndex).
461+
bool previousSuppressState = _suppressTrailingComments;
462+
int previousSuppressIndex = _suppressTrailingCommentsAfterIndex;
463+
if (_options.PreserveComments && _generateSemiColon && !StatementsThatCannotHaveSemiColon.Contains(statement.GetType()))
464+
{
465+
_suppressTrailingComments = true;
466+
_suppressTrailingCommentsAfterIndex = statement.LastTokenIndex;
467+
}
468+
469+
// Generate the statement body
470+
statement.Accept(this);
471+
472+
// Restore suppression state and emit semicolon before trailing comments
473+
_suppressTrailingComments = previousSuppressState;
474+
_suppressTrailingCommentsAfterIndex = previousSuppressIndex;
475+
476+
// Semicolon BEFORE trailing comments
477+
GenerateSemiColonWhenNecessary(statement);
478+
479+
// Now emit trailing comments (after the semicolon)
480+
HandleCommentsAfterFragment(statement);
481+
}
482+
443483
protected void GenerateCommaSeparatedWithClause<T>(IList<T> fragments, bool indent, bool includeParentheses) where T : TSqlFragment
444484
{
445485
if (fragments != null && fragments.Count > 0)

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ public override void ExplicitVisit(IfStatement node)
1818

1919
NewLineAndIndent();
2020
MarkAndPushAlignmentPoint(branchStatements);
21-
GenerateFragmentIfNotNull(node.ThenStatement);
22-
GenerateSemiColonWhenNecessary(node.ThenStatement);
21+
GenerateStatementWithSemiColon(node.ThenStatement);
2322
PopAlignmentPoint();
2423

2524
if (node.ElseStatement != null)
@@ -28,8 +27,7 @@ public override void ExplicitVisit(IfStatement node)
2827
GenerateKeyword(TSqlTokenType.Else);
2928
NewLineAndIndent();
3029
MarkAndPushAlignmentPoint(branchStatements);
31-
GenerateFragmentIfNotNull(node.ElseStatement);
32-
GenerateSemiColonWhenNecessary(node.ElseStatement);
30+
GenerateStatementWithSemiColon(node.ElseStatement);
3331
PopAlignmentPoint();
3432
}
3533
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ public override void ExplicitVisit(StatementList node)
2929
}
3030
}
3131

32-
GenerateFragmentIfNotNull(statement);
33-
GenerateSemiColonWhenNecessary(statement);
32+
GenerateStatementWithSemiColon(statement);
3433
}
3534
}
3635
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ public override void ExplicitVisit(TSqlBatch node)
1313
{
1414
foreach (TSqlStatement statement in node.Statements)
1515
{
16-
GenerateFragmentIfNotNull(statement);
17-
18-
GenerateSemiColonWhenNecessary(statement);
16+
GenerateStatementWithSemiColon(statement);
1917

2018
if (statement is TSqlStatementSnippet == false)
2119
{

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ public override void ExplicitVisit(WhileStatement node)
1818

1919
NewLineAndIndent();
2020
MarkAndPushAlignmentPoint(whileBody);
21-
GenerateFragmentIfNotNull(node.Statement);
22-
GenerateSemiColonWhenNecessary(node.Statement);
21+
GenerateStatementWithSemiColon(node.Statement);
2322
PopAlignmentPoint();
2423
}
2524
}

Test/SqlDom/ScriptGeneratorTests.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,102 @@ public void TestPreserveCommentsEnabled_CommentInNestedSubquery()
12561256
$"inner_q alias should appear before outer_q alias. inner_q at {innerQAliasIdx}, outer_q at {outerQAliasIdx}");
12571257
}
12581258

1259+
[TestMethod]
1260+
[Priority(0)]
1261+
[SqlStudioTestCategory(Category.UnitTest)]
1262+
public void TestPreserveCommentsEnabled_SemicolonBeforeTrailingComment()
1263+
{
1264+
// Test that semicolons are placed BEFORE trailing single-line comments,
1265+
// not after them (which would make the semicolon part of the comment text).
1266+
// Bug fix: previously "SELECT 1 -- comment;" was generated instead of "SELECT 1; -- comment"
1267+
var sqlWithComments = "SELECT 1 -- trailing comment";
1268+
var parser = new TSql170Parser(true);
1269+
var fragment = parser.Parse(new StringReader(sqlWithComments), out var errors);
1270+
1271+
Assert.AreEqual(0, errors.Count);
1272+
1273+
var generatorOptions = new SqlScriptGeneratorOptions
1274+
{
1275+
PreserveComments = true,
1276+
IncludeSemicolons = true
1277+
};
1278+
var generator = new Sql170ScriptGenerator(generatorOptions);
1279+
generator.GenerateScript(fragment, out var generatedSql);
1280+
1281+
// The semicolon must appear BEFORE the trailing comment
1282+
int semicolonIndex = generatedSql.IndexOf(";");
1283+
int commentIndex = generatedSql.IndexOf("-- trailing comment");
1284+
1285+
Assert.IsTrue(semicolonIndex >= 0, "Semicolon should be present in output. Actual: " + generatedSql);
1286+
Assert.IsTrue(commentIndex >= 0, "Trailing comment should be preserved. Actual: " + generatedSql);
1287+
Assert.IsTrue(semicolonIndex < commentIndex,
1288+
$"Semicolon should appear before trailing comment. Semicolon at {semicolonIndex}, comment at {commentIndex}. Actual: " + generatedSql);
1289+
}
1290+
1291+
[TestMethod]
1292+
[Priority(0)]
1293+
[SqlStudioTestCategory(Category.UnitTest)]
1294+
public void TestPreserveCommentsEnabled_SemicolonBeforeTrailingComment_MultipleStatements()
1295+
{
1296+
// Test semicolon placement with multiple statements each having trailing comments
1297+
var sqlWithComments = @"SELECT 1 -- first comment
1298+
SELECT 2 -- second comment";
1299+
var parser = new TSql170Parser(true);
1300+
var fragment = parser.Parse(new StringReader(sqlWithComments), out var errors);
1301+
1302+
Assert.AreEqual(0, errors.Count);
1303+
1304+
var generatorOptions = new SqlScriptGeneratorOptions
1305+
{
1306+
PreserveComments = true,
1307+
IncludeSemicolons = true
1308+
};
1309+
var generator = new Sql170ScriptGenerator(generatorOptions);
1310+
generator.GenerateScript(fragment, out var generatedSql);
1311+
1312+
// Both comments should be preserved
1313+
Assert.IsTrue(generatedSql.Contains("-- first comment"),
1314+
"First trailing comment should be preserved. Actual: " + generatedSql);
1315+
Assert.IsTrue(generatedSql.Contains("-- second comment"),
1316+
"Second trailing comment should be preserved. Actual: " + generatedSql);
1317+
1318+
// Verify semicolons appear before their respective comments, not after
1319+
Assert.IsFalse(generatedSql.Contains("-- first comment;"),
1320+
"Semicolon should not appear after first comment text. Actual: " + generatedSql);
1321+
Assert.IsFalse(generatedSql.Contains("-- second comment;"),
1322+
"Semicolon should not appear after second comment text. Actual: " + generatedSql);
1323+
}
1324+
1325+
[TestMethod]
1326+
[Priority(0)]
1327+
[SqlStudioTestCategory(Category.UnitTest)]
1328+
public void TestPreserveCommentsEnabled_SemicolonBeforeTrailingBlockComment()
1329+
{
1330+
// Test semicolon placement with trailing block comments
1331+
var sqlWithComments = "SELECT 1 /* trailing block comment */";
1332+
var parser = new TSql170Parser(true);
1333+
var fragment = parser.Parse(new StringReader(sqlWithComments), out var errors);
1334+
1335+
Assert.AreEqual(0, errors.Count);
1336+
1337+
var generatorOptions = new SqlScriptGeneratorOptions
1338+
{
1339+
PreserveComments = true,
1340+
IncludeSemicolons = true
1341+
};
1342+
var generator = new Sql170ScriptGenerator(generatorOptions);
1343+
generator.GenerateScript(fragment, out var generatedSql);
1344+
1345+
// The semicolon must appear BEFORE the trailing block comment
1346+
int semicolonIndex = generatedSql.IndexOf(";");
1347+
int commentIndex = generatedSql.IndexOf("/* trailing block comment */");
1348+
1349+
Assert.IsTrue(semicolonIndex >= 0, "Semicolon should be present in output. Actual: " + generatedSql);
1350+
Assert.IsTrue(commentIndex >= 0, "Trailing block comment should be preserved. Actual: " + generatedSql);
1351+
Assert.IsTrue(semicolonIndex < commentIndex,
1352+
$"Semicolon should appear before trailing block comment. Semicolon at {semicolonIndex}, comment at {commentIndex}. Actual: " + generatedSql);
1353+
}
1354+
12591355
#endregion
12601356
}
12611357
}

0 commit comments

Comments
 (0)