@@ -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