44using System . Collections . Generic ;
55using System . Collections . Immutable ;
66using System . Linq ;
7+ using System . Text ;
78using System . Threading ;
89using System . Threading . Tasks ;
910using Microsoft . CodeAnalysis ;
1011using Microsoft . CodeAnalysis . CodeActions ;
11- using Microsoft . CodeAnalysis . CodeFixes . Suppression ;
1212using Microsoft . CodeAnalysis . CodeStyle ;
1313using Microsoft . CodeAnalysis . Editor . Implementation . Preview ;
1414using Microsoft . CodeAnalysis . Editor . UnitTests . Extensions ;
1515using Microsoft . CodeAnalysis . Editor . UnitTests . Workspaces ;
1616using Microsoft . CodeAnalysis . Options ;
17- using Microsoft . CodeAnalysis . Remote ;
1817using Microsoft . CodeAnalysis . Shared . Utilities ;
1918using Microsoft . CodeAnalysis . Test . Utilities ;
2019using Microsoft . CodeAnalysis . Text ;
@@ -37,6 +36,7 @@ public struct TestParameters
3736 internal readonly int index ;
3837 internal readonly CodeActionPriority ? priority ;
3938 internal readonly bool retainNonFixableDiagnostics ;
39+ internal readonly bool includeDiagnosticsOutsideSelection ;
4040 internal readonly string title ;
4141
4242 internal TestParameters (
@@ -47,6 +47,7 @@ internal TestParameters(
4747 int index = 0 ,
4848 CodeActionPriority ? priority = null ,
4949 bool retainNonFixableDiagnostics = false ,
50+ bool includeDiagnosticsOutsideSelection = false ,
5051 string title = null )
5152 {
5253 this . parseOptions = parseOptions ;
@@ -56,23 +57,27 @@ internal TestParameters(
5657 this . index = index ;
5758 this . priority = priority ;
5859 this . retainNonFixableDiagnostics = retainNonFixableDiagnostics ;
60+ this . includeDiagnosticsOutsideSelection = includeDiagnosticsOutsideSelection ;
5961 this . title = title ;
6062 }
6163
6264 public TestParameters WithParseOptions ( ParseOptions parseOptions )
63- => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , title : title ) ;
65+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
6466
6567 public TestParameters WithOptions ( IDictionary < OptionKey , object > options )
66- => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , title : title ) ;
68+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
6769
6870 public TestParameters WithFixProviderData ( object fixProviderData )
69- => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , title : title ) ;
71+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
7072
7173 public TestParameters WithIndex ( int index )
72- => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , title : title ) ;
74+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
7375
7476 public TestParameters WithRetainNonFixableDiagnostics ( bool retainNonFixableDiagnostics )
75- => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , title : title , retainNonFixableDiagnostics : retainNonFixableDiagnostics ) ;
77+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
78+
79+ public TestParameters WithIncludeDiagnosticsOutsideSelection ( bool includeDiagnosticsOutsideSelection )
80+ => new TestParameters ( parseOptions , compilationOptions , options , fixProviderData , index , priority , retainNonFixableDiagnostics , includeDiagnosticsOutsideSelection , title ) ;
7681 }
7782
7883 protected abstract string GetLanguage ( ) ;
@@ -373,17 +378,35 @@ private async Task TestAsync(
373378 CodeActionPriority ? priority ,
374379 TestParameters parameters )
375380 {
381+ MarkupTestFile . GetSpans (
382+ initialMarkup . NormalizeLineEndings ( ) ,
383+ out var initialMarkupWithoutSpans , out IDictionary < string , ImmutableArray < TextSpan > > initialSpanMap ) ;
384+
385+ const string UnnecessaryMarkupKey = "Unnecessary" ;
386+ var unnecessarySpans = initialSpanMap . GetOrAdd ( UnnecessaryMarkupKey , _ => ImmutableArray < TextSpan > . Empty ) ;
387+
376388 MarkupTestFile . GetSpans (
377389 expectedMarkup . NormalizeLineEndings ( ) ,
378- out var expected , out IDictionary < string , ImmutableArray < TextSpan > > spanMap ) ;
390+ out var expected , out IDictionary < string , ImmutableArray < TextSpan > > expectedSpanMap ) ;
379391
380- var conflictSpans = spanMap . GetOrAdd ( "Conflict" , _ => ImmutableArray < TextSpan > . Empty ) ;
381- var renameSpans = spanMap . GetOrAdd ( "Rename" , _ => ImmutableArray < TextSpan > . Empty ) ;
382- var warningSpans = spanMap . GetOrAdd ( "Warning" , _ => ImmutableArray < TextSpan > . Empty ) ;
383- var navigationSpans = spanMap . GetOrAdd ( "Navigation" , _ => ImmutableArray < TextSpan > . Empty ) ;
392+ var conflictSpans = expectedSpanMap . GetOrAdd ( "Conflict" , _ => ImmutableArray < TextSpan > . Empty ) ;
393+ var renameSpans = expectedSpanMap . GetOrAdd ( "Rename" , _ => ImmutableArray < TextSpan > . Empty ) ;
394+ var warningSpans = expectedSpanMap . GetOrAdd ( "Warning" , _ => ImmutableArray < TextSpan > . Empty ) ;
395+ var navigationSpans = expectedSpanMap . GetOrAdd ( "Navigation" , _ => ImmutableArray < TextSpan > . Empty ) ;
384396
385397 using ( var workspace = CreateWorkspaceFromOptions ( initialMarkup , parameters ) )
386398 {
399+ // Ideally this check would always run, but there are several hundred tests that would need to be
400+ // updated with {|Unnecessary:|} spans.
401+ if ( unnecessarySpans . Any ( ) )
402+ {
403+ var allDiagnostics = await GetDiagnosticsWorkerAsync ( workspace , parameters
404+ . WithRetainNonFixableDiagnostics ( true )
405+ . WithIncludeDiagnosticsOutsideSelection ( true ) ) ;
406+
407+ TestDiagnosticTags ( allDiagnostics , unnecessarySpans , WellKnownDiagnosticTags . Unnecessary , UnnecessaryMarkupKey , initialMarkupWithoutSpans ) ;
408+ }
409+
387410 var ( _, action ) = await GetCodeActionsAsync ( workspace , parameters ) ;
388411 await TestActionAsync (
389412 workspace , expected , action ,
@@ -392,6 +415,85 @@ await TestActionAsync(
392415 }
393416 }
394417
418+ private static void TestDiagnosticTags (
419+ ImmutableArray < Diagnostic > diagnostics ,
420+ ImmutableArray < TextSpan > expectedSpans ,
421+ string diagnosticTag ,
422+ string markupKey ,
423+ string initialMarkupWithoutSpans )
424+ {
425+ var diagnosticsWithTag = diagnostics
426+ . Where ( d => d . Descriptor . CustomTags . Contains ( diagnosticTag ) )
427+ . OrderBy ( s => s . Location . SourceSpan . Start )
428+ . ToImmutableArray ( ) ;
429+
430+ if ( expectedSpans . Length != diagnosticsWithTag . Length )
431+ {
432+ AssertEx . Fail ( BuildFailureMessage ( expectedSpans , diagnosticTag , markupKey , initialMarkupWithoutSpans , diagnosticsWithTag ) ) ;
433+ }
434+
435+ for ( var i = 0 ; i < expectedSpans . Length ; i ++ )
436+ {
437+ var actual = diagnosticsWithTag [ i ] . Location . SourceSpan ;
438+ var expected = expectedSpans [ i ] ;
439+ Assert . Equal ( expected , actual ) ;
440+ }
441+ }
442+
443+ private static string BuildFailureMessage (
444+ ImmutableArray < TextSpan > expectedSpans ,
445+ string diagnosticTag ,
446+ string markupKey ,
447+ string initialMarkupWithoutSpans ,
448+ ImmutableArray < Diagnostic > diagnosticsWithTag )
449+ {
450+ var message = $ "Expected { expectedSpans . Length } diagnostic spans with custom tag '{ diagnosticTag } ', but there were { diagnosticsWithTag . Length } .";
451+
452+ if ( expectedSpans . Length == 0 )
453+ {
454+ message += $ " If a diagnostic span tagged '{ diagnosticTag } ' is expected, surround the span in the test markup with the following syntax: {{|Unnecessary:...}}";
455+
456+ var segments = new List < ( int originalStringIndex , string segment ) > ( ) ;
457+
458+ foreach ( var diagnostic in diagnosticsWithTag )
459+ {
460+ var documentOffset = initialMarkupWithoutSpans . IndexOf ( diagnosticsWithTag . First ( ) . Location . SourceTree . ToString ( ) ) ;
461+ if ( documentOffset == - 1 ) continue ;
462+
463+ segments . Add ( ( documentOffset + diagnostic . Location . SourceSpan . Start , "{|" + markupKey + ":" ) ) ;
464+ segments . Add ( ( documentOffset + diagnostic . Location . SourceSpan . End , "|}" ) ) ;
465+ }
466+
467+ if ( segments . Any ( ) )
468+ {
469+ message += Environment . NewLine
470+ + "Example:" + Environment . NewLine
471+ + Environment . NewLine
472+ + InsertSegments ( initialMarkupWithoutSpans , segments ) ;
473+ }
474+ }
475+
476+ return message ;
477+ }
478+
479+ private static string InsertSegments ( string originalString , IEnumerable < ( int originalStringIndex , string segment ) > segments )
480+ {
481+ var builder = new StringBuilder ( ) ;
482+
483+ var positionInOriginalString = 0 ;
484+
485+ foreach ( var ( originalStringIndex , segment ) in segments . OrderBy ( s => s . originalStringIndex ) )
486+ {
487+ builder . Append ( originalString , positionInOriginalString , originalStringIndex - positionInOriginalString ) ;
488+ builder . Append ( segment ) ;
489+
490+ positionInOriginalString = originalStringIndex ;
491+ }
492+
493+ builder . Append ( originalString , positionInOriginalString , originalString . Length - positionInOriginalString ) ;
494+ return builder . ToString ( ) ;
495+ }
496+
395497 internal async Task < Tuple < Solution , Solution > > TestActionAsync (
396498 TestWorkspace workspace , string expected ,
397499 CodeAction action ,
@@ -659,7 +761,7 @@ internal static IDictionary<OptionKey, object> OptionsSet(
659761 /// <summary>
660762 /// Tests all the code actions for the given <paramref name="input"/> string. Each code
661763 /// action must produce the corresponding output in the <paramref name="outputs"/> array.
662- ///
764+ ///
663765 /// Will throw if there are more outputs than code actions or more code actions than outputs.
664766 /// </summary>
665767 protected Task TestAllInRegularAndScriptAsync (
0 commit comments