PreserveComments: trailing single-line comments are re-anchored to a different node, and output is not idempotent
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
Direzione di ricerca
Inizia eseguendo la riproduzione C# in tre passaggi fornita con TSql170Parser, Sql170ScriptGenerator e PreserveComments abilitato. Traccia la gestione dei commenti durante GenerateScript per più commenti finali sui rami CASE WHEN; il lavoro è completato quando i commenti rimangono associati ai relativi rami o alla clausola dell'istruzione e il secondo passaggio di formattazione non produce modifiche.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
With PreserveComments = true, a trailing -- comment attached to a WHEN branch of a CASE expression is emitted against a different node than the one it annotated. Formatting the generated output a second time moves the comment again — this time into the middle of the FROM clause. Output only becomes stable on the third pass, by which point the comment sits in a clause unrelated to the expression it documented.
No exception is thrown and the parser reports no errors on any pass — the only symptom is wrong output, which is what makes it easy to ship unnoticed.
Two problems, one root cause:
- Semantic relocation — a comment that documented
WHEN a = 1ends up annotating the whole select element, and a comment that documentedWHEN a = 2ends up insideFROM. For code where comments carry the rationale for individualCASEbranches, the regenerated script is actively misleading: the text is preserved but the association is lost. - Non-idempotency —
Format(Format(x)) != Format(x). This breaks the usual formatter contract and makes the generator unusable behind aformat-then-verify-no-diffgate, which is how formatters are normally enforced in CI.
Input
SELECT CASE WHEN a = 1 THEN 1 -- one
WHEN a = 2 THEN 2 -- two
ELSE 3 END AS x
FROM t;
Actual output
--- pass 1 (changed: True) ---
SELECT CASE WHEN a = 1 THEN 1 WHEN a = 2 THEN 2 ELSE 3 END AS x -- one
-- two
FROM t;
--- pass 2 (changed: True) ---
SELECT CASE WHEN a = 1 THEN 1 WHEN a = 2 THEN 2 ELSE 3 END AS x -- one
FROM -- two
t;
--- pass 3 (changed: False) ---
SELECT CASE WHEN a = 1 THEN 1 WHEN a = 2 THEN 2 ELSE 3 END AS x -- one
FROM -- two
t;
Note also the stray leading space on the -- two line in pass 1.
A single WHEN branch with a trailing comment is stable — two or more branches are needed to reproduce.
Expected behaviour
Each trailing comment stays attached to the construct it followed in the source, and the second pass is a no-op. Something along these lines would be acceptable:
SELECT CASE WHEN a = 1 THEN 1 -- one
WHEN a = 2 THEN 2 -- two
ELSE 3 END AS x
FROM t;
If per-branch anchoring inside a collapsed expression is not feasible, then keeping every comment within the statement clause it originated in — and guaranteeing idempotency — would still be a large improvement over the current behaviour.
Repro
using Microsoft.SqlServer.TransactSql.ScriptDom;
const string sql = """
SELECT CASE WHEN a = 1 THEN 1 -- one
WHEN a = 2 THEN 2 -- two
ELSE 3 END AS x
FROM t;
""";
static string Format(string input)
{
var parser = new TSql170Parser(true);
var tree = parser.Parse(new StringReader(input), out var errors);
if (errors.Count > 0) throw new Exception(errors[0].Message);
var generator = new Sql170ScriptGenerator(new SqlScriptGeneratorOptions
{
PreserveComments = true
});
generator.GenerateScript(tree, out var output);
return output;
}
var current = sql;
for (var pass = 1; pass <= 3; pass++)
{
var next = Format(current);
Console.WriteLine($"--- pass {pass} (changed: {next != current}) ---");
Console.WriteLine(next);
current = next;
}
Environment
Microsoft.SqlServer.TransactSql.ScriptDom180.78.1 (assembly 18.0.0.0),net8.0- Reproduces identically with
Sql160ScriptGenerator,Sql170ScriptGenerator,Sql180ScriptGenerator(and their matching parsers) - .NET 10, macOS
Impact / context
We evaluated the generator as a formatter for a T-SQL codebase in which comments routinely annotate individual CASE branches. In that setting the relocation is worse than comment loss would be: the text survives, so the output looks fine, but the comment now explains a different expression — plausible enough to pass review unnoticed. The non-idempotency is a separate blocker, since it rules out enforcing the formatter with a format-then-check-for-diff step. Happy to test a fix if that would help.
Related
Adjacent but distinct: #194 (leading newline ahead of multi-line comments) is fixed and covers block comments; #20 is the original PreserveComments request. I could not find an existing report covering trailing single-line comment re-anchoring or the resulting non-idempotency.
- Lingua principale
- GAP
- Stelle
- 277
- Fork
- 43
- Merge medio
- 6g 17h
- PR unite (30g)
- 3
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di microsoft/SqlScriptDOM
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
microsoft/SqlScriptDOM#228 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
microsoft/SqlScriptDOM#183 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 56/100
microsoft/SqlScriptDOM#226 · 1 reazione ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 58/100
microsoft/SqlScriptDOM#224 ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 68/100
microsoft/SqlScriptDOM#222 ·
Tutte le issue di microsoft/SqlScriptDOM
Issue simili
-
ZCode 3.14.3 に対応する Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
supermomonga/zcode-acp#24 ·
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 90/100
learningequality/ricecooker#747 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
KhronosGroup/glTF-Blender-IO#2769 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100