@@ -12,7 +12,9 @@ namespace StyleCop.Analyzers.ReadabilityRules
1212 using Microsoft . CodeAnalysis ;
1313 using Microsoft . CodeAnalysis . CodeActions ;
1414 using Microsoft . CodeAnalysis . CodeFixes ;
15+ using Microsoft . CodeAnalysis . CSharp ;
1516 using Microsoft . CodeAnalysis . Text ;
17+ using Settings . ObjectModel ;
1618
1719 [ ExportCodeFixProvider ( LanguageNames . CSharp , Name = nameof ( IndentationCodeFixProvider ) ) ]
1820 [ Shared ]
@@ -26,7 +28,7 @@ internal class IndentationCodeFixProvider : CodeFixProvider
2628
2729 /// <inheritdoc/>
2830 public sealed override FixAllProvider GetFixAllProvider ( ) =>
29- FixAll . Instance ;
31+ null ;
3032
3133 /// <inheritdoc/>
3234 public override Task RegisterCodeFixesAsync ( CodeFixContext context )
@@ -48,26 +50,28 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
4850 {
4951 var syntaxRoot = await document . GetSyntaxRootAsync ( ) . ConfigureAwait ( false ) ;
5052
51- TextChange textChange ;
52- if ( ! TryGetTextChange ( diagnostic , syntaxRoot , out textChange ) )
53+ StyleCopSettings settings = SettingsHelper . GetStyleCopSettings ( document . Project . AnalyzerOptions , cancellationToken ) ;
54+ ImmutableArray < TextChange > textChanges = await GetTextChangesAsync ( diagnostic , syntaxRoot , settings . Indentation , cancellationToken ) . ConfigureAwait ( false ) ;
55+ if ( textChanges . IsEmpty )
5356 {
5457 return document ;
5558 }
5659
5760 var text = await document . GetTextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
58- return document . WithText ( text . WithChanges ( textChange ) ) ;
61+ return document . WithText ( text . WithChanges ( textChanges ) ) ;
5962 }
6063
61- private static bool TryGetTextChange ( Diagnostic diagnostic , SyntaxNode syntaxRoot , out TextChange textChange )
64+ private static async Task < ImmutableArray < TextChange > > GetTextChangesAsync ( Diagnostic diagnostic , SyntaxNode syntaxRoot , IndentationSettings indentationSettings , CancellationToken cancellationToken )
6265 {
6366 string replacement ;
6467 if ( ! diagnostic . Properties . TryGetValue ( SA1137ElementsShouldHaveTheSameIndentation . ExpectedIndentationKey , out replacement ) )
6568 {
66- textChange = default ( TextChange ) ;
67- return false ;
69+ return ImmutableArray < TextChange > . Empty ;
6870 }
6971
70- var trivia = syntaxRoot . FindTrivia ( diagnostic . Location . SourceSpan . Start ) ;
72+ SyntaxTrivia trivia = syntaxRoot . FindTrivia ( diagnostic . Location . SourceSpan . Start ) ;
73+ SyntaxToken token = trivia != default ( SyntaxTrivia ) ? trivia . Token : syntaxRoot . FindToken ( diagnostic . Location . SourceSpan . Start , findInsideTrivia : true ) ;
74+ SyntaxNode node = GetNodeForAdjustment ( token ) ;
7175
7276 TextSpan originalSpan ;
7377 if ( trivia == default ( SyntaxTrivia ) )
@@ -80,8 +84,139 @@ private static bool TryGetTextChange(Diagnostic diagnostic, SyntaxNode syntaxRoo
8084 originalSpan = trivia . Span ;
8185 }
8286
83- textChange = new TextChange ( originalSpan , replacement ) ;
84- return true ;
87+ FileLinePositionSpan fullSpan = syntaxRoot . SyntaxTree . GetLineSpan ( node . FullSpan , cancellationToken ) ;
88+ if ( fullSpan . StartLinePosition . Line == fullSpan . EndLinePosition . Line )
89+ {
90+ return ImmutableArray . Create ( new TextChange ( originalSpan , replacement ) ) ;
91+ }
92+
93+ SyntaxTree tree = node . SyntaxTree ;
94+ SourceText sourceText = await tree . GetTextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
95+
96+ int originalIndentation = GetIndentationWidth ( indentationSettings , sourceText . ToString ( originalSpan ) ) ;
97+ int newIndentation = GetIndentationWidth ( indentationSettings , replacement ) ;
98+
99+ ImmutableArray < TextSpan > excludedSpans = SyntaxTreeHelpers . GetExcludedSpans ( node ) ;
100+ TextLineCollection lines = sourceText . Lines ;
101+
102+ // For each line in the full span of the syntax node:
103+ // 1. If the line is indented less than originalIndentation, ignore the line
104+ // 2. If the indentation characters are not located within the full span, ignore the line
105+ // 2. If the indentation characters of the line overlap with an excluded span, ignore the line
106+ // 3. Replace the first original.Length characters on the line with replacement
107+ ImmutableArray < TextChange > . Builder builder = ImmutableArray . CreateBuilder < TextChange > ( ) ;
108+ for ( int i = fullSpan . StartLinePosition . Line ; i <= fullSpan . EndLinePosition . Line ; i ++ )
109+ {
110+ TextLine line = lines [ i ] ;
111+ string lineText = sourceText . ToString ( line . Span ) ;
112+
113+ int indentationCount ;
114+ int indentationWidth = GetIndentationWidth ( indentationSettings , lineText , out indentationCount ) ;
115+ if ( indentationWidth < originalIndentation )
116+ {
117+ continue ;
118+ }
119+
120+ if ( indentationCount == line . Span . Length )
121+ {
122+ // The line is just whitespace
123+ continue ;
124+ }
125+
126+ TextSpan indentationSpan = new TextSpan ( line . Start , indentationCount ) ;
127+ if ( indentationSpan . Start >= node . FullSpan . End )
128+ {
129+ // The line does not contain any non-whitespace content which is part of the full span of the node
130+ continue ;
131+ }
132+
133+ if ( ! node . FullSpan . Contains ( indentationSpan ) )
134+ {
135+ // The indentation of the line is not part of the full span of the node
136+ continue ;
137+ }
138+
139+ if ( IsExcluded ( excludedSpans , indentationSpan ) )
140+ {
141+ // The line indentation is partially- or fully-excluded from adjustments
142+ continue ;
143+ }
144+
145+ if ( originalIndentation == indentationWidth )
146+ {
147+ builder . Add ( new TextChange ( indentationSpan , replacement ) ) ;
148+ }
149+ else if ( newIndentation > originalIndentation )
150+ {
151+ // TODO: This needs to handle UseTabs setting
152+ builder . Add ( new TextChange ( new TextSpan ( indentationSpan . End , 0 ) , new string ( ' ' , newIndentation - originalIndentation ) ) ) ;
153+ }
154+ else if ( newIndentation < originalIndentation )
155+ {
156+ builder . Add ( new TextChange ( indentationSpan , IndentationHelper . GenerateIndentationString ( indentationSettings , indentationWidth + ( newIndentation - originalIndentation ) ) ) ) ;
157+ }
158+ }
159+
160+ return builder . ToImmutable ( ) ;
161+ }
162+
163+ private static SyntaxNode GetNodeForAdjustment ( SyntaxToken token )
164+ {
165+ return token . Parent ;
166+ }
167+
168+ private static int GetIndentationWidth ( IndentationSettings indentationSettings , string text )
169+ {
170+ int ignored ;
171+ return GetIndentationWidth ( indentationSettings , text , out ignored ) ;
172+ }
173+
174+ private static int GetIndentationWidth ( IndentationSettings indentationSettings , string text , out int count )
175+ {
176+ int tabSize = indentationSettings . TabSize ;
177+ int indentationWidth = 0 ;
178+ for ( int i = 0 ; i < text . Length ; i ++ )
179+ {
180+ switch ( text [ i ] )
181+ {
182+ case ' ' :
183+ indentationWidth ++ ;
184+ break ;
185+
186+ case '\t ' :
187+ indentationWidth = tabSize * ( ( indentationWidth / tabSize ) + 1 ) ;
188+ break ;
189+
190+ default :
191+ count = i ;
192+ return indentationWidth ;
193+ }
194+ }
195+
196+ count = text . Length ;
197+ return indentationWidth ;
198+ }
199+
200+ private static bool IsExcluded ( ImmutableArray < TextSpan > excludedSpans , TextSpan textSpan )
201+ {
202+ int index = excludedSpans . BinarySearch ( textSpan ) ;
203+ if ( index > 0 )
204+ {
205+ return true ;
206+ }
207+
208+ int nextLarger = ~ index ;
209+ if ( nextLarger > 0 && excludedSpans [ nextLarger - 1 ] . OverlapsWith ( textSpan ) )
210+ {
211+ return true ;
212+ }
213+
214+ if ( nextLarger < excludedSpans . Length - 1 && excludedSpans [ nextLarger ] . OverlapsWith ( textSpan ) )
215+ {
216+ return true ;
217+ }
218+
219+ return false ;
85220 }
86221
87222 private class FixAll : DocumentBasedFixAllProvider
@@ -100,16 +235,13 @@ protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fi
100235 }
101236
102237 var syntaxRoot = await document . GetSyntaxRootAsync ( ) . ConfigureAwait ( false ) ;
238+ StyleCopSettings settings = SettingsHelper . GetStyleCopSettings ( document . Project . AnalyzerOptions , fixAllContext . CancellationToken ) ;
103239
104240 List < TextChange > changes = new List < TextChange > ( ) ;
105241
106242 foreach ( var diagnostic in diagnostics )
107243 {
108- TextChange textChange ;
109- if ( TryGetTextChange ( diagnostic , syntaxRoot , out textChange ) )
110- {
111- changes . Add ( textChange ) ;
112- }
244+ changes . AddRange ( await GetTextChangesAsync ( diagnostic , syntaxRoot , settings . Indentation , fixAllContext . CancellationToken ) . ConfigureAwait ( false ) ) ;
113245 }
114246
115247 changes . Sort ( ( left , right ) => left . Span . Start . CompareTo ( right . Span . Start ) ) ;
0 commit comments