Skip to content

Commit d97ddd4

Browse files
authored
Improve pipeline indentation handling in UseConsistentIndentation rule (#2173)
* Improve pipeline indentation handling in UseConsistentIndentation rule * Fix over-indentation when multiple openers appear on the same line
1 parent a143b9f commit d97ddd4

2 files changed

Lines changed: 626 additions & 39 deletions

File tree

Rules/UseConsistentIndentation.cs

Lines changed: 146 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,27 @@ public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string file
130130
var tokens = Helper.Instance.Tokens;
131131
var diagnosticRecords = new List<DiagnosticRecord>();
132132
var indentationLevel = 0;
133-
var currentIndenationLevelIncreaseDueToPipelines = 0;
134133
var onNewLine = true;
135134
var pipelineAsts = ast.FindAll(testAst => testAst is PipelineAst && (testAst as PipelineAst).PipelineElements.Count > 1, true).ToList();
136-
/*
137-
When an LParen and LBrace are on the same line, it can lead to too much de-indentation.
138-
In order to prevent the RParen code from de-indenting too much, we keep a stack of when we skipped the indentation
139-
caused by tokens that require a closing RParen (which are LParen, AtParen and DollarParen).
140-
*/
141-
var lParenSkippedIndentation = new Stack<bool>();
142-
135+
// Sort by end position so that inner (nested) pipelines appear before outer ones.
136+
// This is required by MatchingPipelineAstEnd, whose early-break optimization
137+
// would otherwise skip nested pipelines that end before their outer pipeline.
138+
pipelineAsts.Sort((a, b) =>
139+
{
140+
int lineCmp = a.Extent.EndScriptPosition.LineNumber.CompareTo(b.Extent.EndScriptPosition.LineNumber);
141+
return lineCmp != 0 ? lineCmp : a.Extent.EndScriptPosition.ColumnNumber.CompareTo(b.Extent.EndScriptPosition.ColumnNumber);
142+
});
143+
// Track pipeline indentation increases per PipelineAst instead of as a single
144+
// flat counter. A flat counter caused all accumulated pipeline indentation to be
145+
// subtracted when *any* pipeline ended, instead of only the contribution from
146+
// that specific pipeline - leading to runaway indentation with nested pipelines.
147+
var pipelineIndentationIncreases = new Dictionary<PipelineAst, int>();
148+
// When multiple openers appear on the same line (e.g. ({ or @(@{),
149+
// only the last unclosed opener should affect indentation. We
150+
// track, for every opener, whether its indentation increment was
151+
// skipped so that the matching closer knows not to decrement.
152+
var openerSkippedIndentation = new Stack<bool>();
153+
143154
for (int tokenIndex = 0; tokenIndex < tokens.Length; tokenIndex++)
144155
{
145156
var token = tokens[tokenIndex];
@@ -153,27 +164,39 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do
153164
{
154165
case TokenKind.AtCurly:
155166
case TokenKind.LCurly:
156-
AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine);
157-
break;
158-
159167
case TokenKind.DollarParen:
160168
case TokenKind.AtParen:
161-
lParenSkippedIndentation.Push(false);
162-
AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine);
169+
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
170+
if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex))
171+
{
172+
openerSkippedIndentation.Push(true);
173+
}
174+
else
175+
{
176+
indentationLevel++;
177+
openerSkippedIndentation.Push(false);
178+
}
163179
break;
164180

165181
case TokenKind.LParen:
166182
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
167-
// When a line starts with a parenthesis and it is not the last non-comment token of that line,
168-
// then indentation does not need to be increased.
183+
// When a line starts with a parenthesis and it is not the
184+
// last non-comment token of that line, indentation does
185+
// not need to be increased.
169186
if ((tokenIndex == 0 || tokens[tokenIndex - 1].Kind == TokenKind.NewLine) &&
170187
NextTokenIgnoringComments(tokens, tokenIndex)?.Kind != TokenKind.NewLine)
171188
{
172-
onNewLine = false;
173-
lParenSkippedIndentation.Push(true);
189+
openerSkippedIndentation.Push(true);
174190
break;
175191
}
176-
lParenSkippedIndentation.Push(false);
192+
// General case: skip when another opener follows so that
193+
// only the last unclosed opener on a line is indent-affecting.
194+
if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex))
195+
{
196+
openerSkippedIndentation.Push(true);
197+
break;
198+
}
199+
openerSkippedIndentation.Push(false);
177200
indentationLevel++;
178201
break;
179202

@@ -188,40 +211,50 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do
188211
if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline)
189212
{
190213
AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine);
191-
currentIndenationLevelIncreaseDueToPipelines++;
214+
// Attribute this increase to the innermost pipeline containing
215+
// this pipe token so it is only reversed when that specific
216+
// pipeline ends, not when an unrelated outer pipeline ends.
217+
PipelineAst containingPipeline = FindInnermostContainingPipeline(pipelineAsts, token);
218+
if (containingPipeline != null)
219+
{
220+
if (!pipelineIndentationIncreases.ContainsKey(containingPipeline))
221+
pipelineIndentationIncreases[containingPipeline] = 0;
222+
pipelineIndentationIncreases[containingPipeline]++;
223+
}
192224
break;
193225
}
194226
if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline)
195227
{
196-
bool isFirstPipeInPipeline = pipelineAsts.Any(pipelineAst =>
197-
PositionIsEqual(LastPipeOnFirstLineWithPipeUsage((PipelineAst)pipelineAst).Extent.EndScriptPosition,
198-
tokens[tokenIndex - 1].Extent.EndScriptPosition));
199-
if (isFirstPipeInPipeline)
228+
// Capture which specific PipelineAst this is the first pipe for,
229+
// so the indentation increase is attributed to that pipeline only.
230+
PipelineAst firstPipePipeline = pipelineAsts
231+
.Cast<PipelineAst>()
232+
.FirstOrDefault(pipelineAst =>
233+
PositionIsEqual(LastPipeOnFirstLineWithPipeUsage(pipelineAst).Extent.EndScriptPosition,
234+
tokens[tokenIndex - 1].Extent.EndScriptPosition));
235+
if (firstPipePipeline != null)
200236
{
201237
AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine);
202-
currentIndenationLevelIncreaseDueToPipelines++;
238+
if (!pipelineIndentationIncreases.ContainsKey(firstPipePipeline))
239+
pipelineIndentationIncreases[firstPipePipeline] = 0;
240+
pipelineIndentationIncreases[firstPipePipeline]++;
203241
}
204242
}
205243
break;
206244

207245
case TokenKind.RParen:
208-
bool matchingLParenIncreasedIndentation = false;
209-
if (lParenSkippedIndentation.Count > 0)
246+
case TokenKind.RCurly:
247+
if (openerSkippedIndentation.Count > 0 && openerSkippedIndentation.Pop())
210248
{
211-
matchingLParenIncreasedIndentation = lParenSkippedIndentation.Pop();
249+
// The matching opener skipped its increment, so we
250+
// skip the decrement but still enforce indentation.
251+
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
212252
}
213-
if (matchingLParenIncreasedIndentation)
253+
else
214254
{
215-
onNewLine = false;
216-
break;
255+
indentationLevel = ClipNegative(indentationLevel - 1);
256+
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
217257
}
218-
indentationLevel = ClipNegative(indentationLevel - 1);
219-
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
220-
break;
221-
222-
case TokenKind.RCurly:
223-
indentationLevel = ClipNegative(indentationLevel - 1);
224-
AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine);
225258
break;
226259

227260
case TokenKind.NewLine:
@@ -290,14 +323,62 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do
290323
if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline ||
291324
pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline)
292325
{
293-
indentationLevel = ClipNegative(indentationLevel - currentIndenationLevelIncreaseDueToPipelines);
294-
currentIndenationLevelIncreaseDueToPipelines = 0;
326+
// Only subtract the indentation contributed by this specific pipeline,
327+
// leaving contributions from outer/unrelated pipelines intact.
328+
if (pipelineIndentationIncreases.TryGetValue(matchingPipeLineAstEnd, out int contribution))
329+
{
330+
indentationLevel = ClipNegative(indentationLevel - contribution);
331+
pipelineIndentationIncreases.Remove(matchingPipeLineAstEnd);
332+
}
295333
}
296334
}
297335

298336
return diagnosticRecords;
299337
}
300338

339+
/// <summary>
340+
/// Scans forward from the current opener to the end of the line.
341+
/// Returns true if there is at least one unclosed opener when
342+
/// the line ends, meaning the current opener should skip its
343+
/// indentation increment. If the current opener's own closer
344+
/// is found on the same line (depth drops below zero), returns
345+
/// false so that it indents normally.
346+
/// </summary>
347+
private static bool HasUnclosedOpenerBeforeLineEnd(Token[] tokens, int currentIndex)
348+
{
349+
int depth = 0;
350+
for (int i = currentIndex + 1; i < tokens.Length; i++)
351+
{
352+
switch (tokens[i].Kind)
353+
{
354+
case TokenKind.NewLine:
355+
case TokenKind.LineContinuation:
356+
case TokenKind.EndOfInput:
357+
return depth > 0;
358+
359+
case TokenKind.LCurly:
360+
case TokenKind.AtCurly:
361+
case TokenKind.LParen:
362+
case TokenKind.AtParen:
363+
case TokenKind.DollarParen:
364+
depth++;
365+
break;
366+
367+
case TokenKind.RCurly:
368+
case TokenKind.RParen:
369+
depth--;
370+
if (depth < 0)
371+
{
372+
// Our own closer was found on this line.
373+
return false;
374+
}
375+
break;
376+
}
377+
}
378+
379+
return depth > 0;
380+
}
381+
301382
private static Token NextTokenIgnoringComments(Token[] tokens, int startIndex)
302383
{
303384
if (startIndex >= tokens.Length - 1)
@@ -432,6 +513,32 @@ private static PipelineAst MatchingPipelineAstEnd(List<Ast> pipelineAsts, Token
432513
return matchingPipeLineAstEnd;
433514
}
434515

516+
/// <summary>
517+
/// Finds the innermost (smallest) PipelineAst whose extent fully contains the given token.
518+
/// Used to attribute pipeline indentation increases to the correct pipeline when
519+
/// using IncreaseIndentationAfterEveryPipeline.
520+
/// </summary>
521+
private static PipelineAst FindInnermostContainingPipeline(List<Ast> pipelineAsts, Token token)
522+
{
523+
PipelineAst best = null;
524+
int bestSize = int.MaxValue;
525+
foreach (var ast in pipelineAsts)
526+
{
527+
var pipeline = (PipelineAst)ast;
528+
int pipelineStart = pipeline.Extent.StartOffset;
529+
int pipelineEnd = pipeline.Extent.EndOffset;
530+
int pipelineSize = pipelineEnd - pipelineStart;
531+
if (pipelineStart <= token.Extent.StartOffset &&
532+
token.Extent.EndOffset <= pipelineEnd &&
533+
pipelineSize < bestSize)
534+
{
535+
best = pipeline;
536+
bestSize = pipelineSize;
537+
}
538+
}
539+
return best;
540+
}
541+
435542
private static bool PositionIsEqual(IScriptPosition position1, IScriptPosition position2)
436543
{
437544
return position1.ColumnNumber == position2.ColumnNumber &&

0 commit comments

Comments
 (0)