/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs
5 120 строк
244 KB
Chris Sienkiewicz
Fix input nodes incorrectly picking duplicate elements (#83878)
29 май 2026, 21:07
Не верифицирован
29 май 2026, 21:07
74d6b9a
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.CSharp.UnitTests; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.SourceGeneration { public class GeneratorDriverTests(ITestOutputHelper output) : CSharpTestBase { private readonly ITestOutputHelper _output = output; [Fact] public void Running_With_No_Changes_Is_NoOp() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); GeneratorDriver driver = CSharpGeneratorDriver.Create(ImmutableArray<ISourceGenerator>.Empty, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); Assert.Empty(diagnostics); Assert.Single(outputCompilation.SyntaxTrees); Assert.Equal(compilation, outputCompilation); } [Fact] public void Generator_Is_Initialized_Before_Running() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int initCount = 0, executeCount = 0; var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); Assert.Equal(1, initCount); Assert.Equal(1, executeCount); } [Fact] public void Generator_Is_Not_Initialized_If_Not_Run() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int initCount = 0, executeCount = 0; var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); Assert.Equal(0, initCount); Assert.Equal(0, executeCount); } [Fact] public void Generator_Is_Only_Initialized_Once() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int initCount = 0, executeCount = 0; var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++, source: "public class C { }"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); driver = driver.RunGeneratorsAndUpdateCompilation(outputCompilation, out outputCompilation, out _); driver.RunGeneratorsAndUpdateCompilation(outputCompilation, out outputCompilation, out _); Assert.Equal(1, initCount); Assert.Equal(3, executeCount); } [Fact] public void Single_File_Is_Added() { var source = @" class C { } "; var generatorSource = @" class GeneratedClass { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); SingleFileTestGenerator testGenerator = new SingleFileTestGenerator(generatorSource); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); Assert.NotEqual(compilation, outputCompilation); var generatedClass = outputCompilation.GlobalNamespace.GetTypeMembers("GeneratedClass").Single(); Assert.True(generatedClass.Locations.Single().IsInSource); } [Fact] public void Analyzer_Is_Run() { var source = @" class C { } "; var generatorSource = @" class GeneratedClass { } "; var parseOptions = TestOptions.Regular; var analyzer = new Analyzer_Is_Run_Analyzer(); Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); compilation.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); Assert.Equal(0, analyzer.GeneratedClassCount); SingleFileTestGenerator testGenerator = new SingleFileTestGenerator(generatorSource); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); outputCompilation.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); Assert.Equal(1, analyzer.GeneratedClassCount); } private class Analyzer_Is_Run_Analyzer : DiagnosticAnalyzer { public int GeneratedClassCount; private static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor); public override void Initialize(AnalysisContext context) { context.RegisterSymbolAction(Handle, SymbolKind.NamedType); } private void Handle(SymbolAnalysisContext context) { switch (context.Symbol.ToTestDisplayString()) { case "GeneratedClass": Interlocked.Increment(ref GeneratedClassCount); break; case "C": case "System.Runtime.CompilerServices.IsExternalInit": break; default: Assert.True(false); break; } } } [Fact] public void Single_File_Is_Added_OnlyOnce_For_Multiple_Calls() { var source = @" class C { } "; var generatorSource = @" class GeneratedClass { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); SingleFileTestGenerator testGenerator = new SingleFileTestGenerator(generatorSource); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation1, out _); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation2, out _); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation3, out _); Assert.Equal(2, outputCompilation1.SyntaxTrees.Count()); Assert.Equal(2, outputCompilation2.SyntaxTrees.Count()); Assert.Equal(2, outputCompilation3.SyntaxTrees.Count()); Assert.NotEqual(compilation, outputCompilation1); Assert.NotEqual(compilation, outputCompilation2); Assert.NotEqual(compilation, outputCompilation3); } [Fact] public void User_Source_Can_Depend_On_Generated_Source() { var source = @" #pragma warning disable CS0649 class C { public D d; } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics( // (5,12): error CS0246: The type or namespace name 'D' could not be found (are you missing a using directive or an assembly reference?) // public D d; Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "D").WithArguments("D").WithLocation(5, 12) ); Assert.Single(compilation.SyntaxTrees); var generator = new SingleFileTestGenerator("public class D { }"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); } [Fact] public void Error_During_Initialization_Is_Reported() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("init error"); var generator = new CallbackGenerator((ic) => throw exception, (sgc) => { }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "init error", initialization: true); } [Fact] public void Error_During_Initialization_Generator_Does_Not_Run() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("init error"); var generator = new CallbackGenerator((ic) => throw exception, (sgc) => { }, source: "class D { }"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); Assert.Single(outputCompilation.SyntaxTrees); } [Fact] public void Error_During_Generation_Is_Reported() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("generate error"); var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "generate error"); } [Fact] public void Error_During_Generation_Does_Not_Affect_Other_Generators() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("generate error"); var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception); var generator2 = new CallbackGenerator2((ic) => { }, (sgc) => { }, source: "public class D { }"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "generate error"); } [Fact] public void Error_During_Generation_With_Dependent_Source() { var source = @" #pragma warning disable CS0649 class C { public D d; } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics( // (5,12): error CS0246: The type or namespace name 'D' could not be found (are you missing a using directive or an assembly reference?) // public D d; Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "D").WithArguments("D").WithLocation(5, 12) ); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("generate error"); var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception, source: "public class D { }"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics( // (5,12): error CS0246: The type or namespace name 'D' could not be found (are you missing a using directive or an assembly reference?) // public D d; Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "D").WithArguments("D").WithLocation(5, 12) ); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "generate error"); } [Fact] public void Error_During_Generation_Has_Exception_In_Description() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var exception = new InvalidOperationException("generate error"); var generator = new CallbackGenerator((ic) => { }, (sgc) => throw exception); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); // Since translated description strings can have punctuation that differs based on locale, simply ensure the // exception message is contained in the diagnostic text. Assert.Contains(exception.ToString(), generatorDiagnostics.Single().ToString()); } [Fact] public void Generator_Can_Report_Diagnostics() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); string description = "This is a test diagnostic"; DiagnosticDescriptor generatorDiagnostic = new DiagnosticDescriptor("TG001", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); var diagnostic = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic, Location.None); var generator = new CallbackGenerator((ic) => { }, (sgc) => sgc.ReportDiagnostic(diagnostic)); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify( Diagnostic("TG001").WithLocation(1, 1) ); } [Fact] public void Generator_HintName_MustBe_Unique() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8)); // the assert should swallow the exception, so we'll actually successfully generate Assert.Throws<ArgumentException>("hintName", () => sgc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8))); // also throws for <name> vs <name>.cs Assert.Throws<ArgumentException>("hintName", () => sgc.AddSource("test.cs", SourceText.From("public class D{}", Encoding.UTF8))); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); } [ConditionalFact(typeof(IsEnglishLocal))] public void Generator_HintName_MustBe_Unique_Across_Outputs() { var source = @" class C { } "; var parseOptions = TestOptions.Regular.WithLanguageVersion(LanguageVersion.Preview); Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8)); // throws immediately, because we're within the same output node Assert.Throws<ArgumentException>("hintName", () => spc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8))); // throws for .cs too Assert.Throws<ArgumentException>("hintName", () => spc.AddSource("test.cs", SourceText.From("public class D{}", Encoding.UTF8))); }); ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { // will not throw at this point, because we have no way of knowing what the other outputs added // we *will* throw later in the driver when we combine them however (this is a change for V2, but not visible from V1) spc.AddSource("test", SourceText.From("public class D{}", Encoding.UTF8)); }); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); VerifyArgumentExceptionDiagnostic(generatorDiagnostics.Single(), nameof(PipelineCallbackGenerator), "The hintName 'test.cs' of the added source file must be unique within a generator.", "hintName"); Assert.Equal(1, outputCompilation.SyntaxTrees.Count()); } [Fact] public void Generator_HintName_Is_Appended_With_GeneratorName() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new SingleFileTestGenerator("public class D {}", "source.cs"); var generator2 = new SingleFileTestGenerator2("public class E {}", "source.cs"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); Assert.Equal(3, outputCompilation.SyntaxTrees.Count()); var filePaths = outputCompilation.SyntaxTrees.Skip(1).Select(t => t.FilePath).ToArray(); Assert.Equal(new[] { Path.Combine(generator.GetType().Assembly.GetName().Name!, generator.GetType().FullName!, "source.cs"), Path.Combine(generator2.GetType().Assembly.GetName().Name!, generator2.GetType().FullName!, "source.cs") }, filePaths); } [Fact] public void RunResults_Are_Empty_Before_Generation() { GeneratorDriver driver = CSharpGeneratorDriver.Create(ImmutableArray<ISourceGenerator>.Empty, parseOptions: TestOptions.Regular); var results = driver.GetRunResult(); Assert.Empty(results.GeneratedTrees); Assert.Empty(results.Diagnostics); Assert.Empty(results.Results); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/74033")] public void RunResults_Are_Empty_Before_Generation_With_Generators() { var generator = new SingleFileTestGenerator("public class D {}", "source.cs"); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], parseOptions: TestOptions.Regular); var results = driver.GetRunResult(); Assert.Empty(results.GeneratedTrees); Assert.Empty(results.Diagnostics); var result = Assert.Single(results.Results); Assert.Null(result.Exception); Assert.True(result.Diagnostics.IsDefault); Assert.True(result.GeneratedSources.IsDefault); Assert.Null(result.TrackedSteps); Assert.Null(result.TrackedOutputSteps); Assert.Equal(TimeSpan.Zero, result.ElapsedTime); Assert.Equal(generator, result.Generator); } [Fact] public void RunResults_Are_Available_After_Generation() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.AddSource("test", SourceText.From("public class D {}", Encoding.UTF8)); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Single(results.GeneratedTrees); Assert.Single(results.Results); Assert.Empty(results.Diagnostics); var result = results.Results.Single(); Assert.Null(result.Exception); Assert.Empty(result.Diagnostics); Assert.Single(result.GeneratedSources); Assert.Equal(results.GeneratedTrees.Single(), result.GeneratedSources.Single().SyntaxTree); } [Fact] public void RunResults_Combine_SyntaxTrees() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.AddSource("test", SourceText.From("public class D {}", Encoding.UTF8)); sgc.AddSource("test2", SourceText.From("public class E {}", Encoding.UTF8)); }); var generator2 = new SingleFileTestGenerator("public class F{}"); var generator3 = new SingleFileTestGenerator2("public class G{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator, generator2, generator3 }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Equal(4, results.GeneratedTrees.Length); Assert.Equal(3, results.Results.Length); Assert.Empty(results.Diagnostics); var result1 = results.Results[0]; var result2 = results.Results[1]; var result3 = results.Results[2]; Assert.Null(result1.Exception); Assert.Empty(result1.Diagnostics); Assert.Equal(2, result1.GeneratedSources.Length); Assert.Equal(results.GeneratedTrees[0], result1.GeneratedSources[0].SyntaxTree); Assert.Equal(results.GeneratedTrees[1], result1.GeneratedSources[1].SyntaxTree); Assert.Null(result2.Exception); Assert.Empty(result2.Diagnostics); Assert.Single(result2.GeneratedSources); Assert.Equal(results.GeneratedTrees[2], result2.GeneratedSources[0].SyntaxTree); Assert.Null(result3.Exception); Assert.Empty(result3.Diagnostics); Assert.Single(result3.GeneratedSources); Assert.Equal(results.GeneratedTrees[3], result3.GeneratedSources[0].SyntaxTree); } [Fact] public void RunResults_Combine_Diagnostics() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); string description = "This is a test diagnostic"; DiagnosticDescriptor generatorDiagnostic1 = new DiagnosticDescriptor("TG001", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); DiagnosticDescriptor generatorDiagnostic2 = new DiagnosticDescriptor("TG002", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); DiagnosticDescriptor generatorDiagnostic3 = new DiagnosticDescriptor("TG003", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); var diagnostic1 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic1, Location.None); var diagnostic2 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic2, Location.None); var diagnostic3 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic3, Location.None); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.ReportDiagnostic(diagnostic1); sgc.ReportDiagnostic(diagnostic2); }); var generator2 = new CallbackGenerator2((ic) => { }, (sgc) => { sgc.ReportDiagnostic(diagnostic3); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Equal(2, results.Results.Length); Assert.Equal(3, results.Diagnostics.Length); Assert.Empty(results.GeneratedTrees); var result1 = results.Results[0]; var result2 = results.Results[1]; Assert.Null(result1.Exception); Assert.Equal(2, result1.Diagnostics.Length); Assert.Empty(result1.GeneratedSources); Assert.Equal(results.Diagnostics[0], result1.Diagnostics[0]); Assert.Equal(results.Diagnostics[1], result1.Diagnostics[1]); Assert.Null(result2.Exception); Assert.Single(result2.Diagnostics); Assert.Empty(result2.GeneratedSources); Assert.Equal(results.Diagnostics[2], result2.Diagnostics[0]); } [Fact] public void FullGeneration_Diagnostics_AreSame_As_RunResults() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); string description = "This is a test diagnostic"; DiagnosticDescriptor generatorDiagnostic1 = new DiagnosticDescriptor("TG001", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); DiagnosticDescriptor generatorDiagnostic2 = new DiagnosticDescriptor("TG002", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); DiagnosticDescriptor generatorDiagnostic3 = new DiagnosticDescriptor("TG003", "Test Diagnostic", description, "Generators", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: description); var diagnostic1 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic1, Location.None); var diagnostic2 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic2, Location.None); var diagnostic3 = Microsoft.CodeAnalysis.Diagnostic.Create(generatorDiagnostic3, Location.None); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.ReportDiagnostic(diagnostic1); sgc.ReportDiagnostic(diagnostic2); }); var generator2 = new CallbackGenerator2((ic) => { }, (sgc) => { sgc.ReportDiagnostic(diagnostic3); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var fullDiagnostics); var results = driver.GetRunResult(); Assert.Equal(3, results.Diagnostics.Length); Assert.Equal(3, fullDiagnostics.Length); AssertEx.Equal(results.Diagnostics, fullDiagnostics); } [Fact] public void Cancellation_During_Execution_Doesnt_Report_As_Generator_Error() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); CancellationTokenSource cts = new CancellationTokenSource(); var testGenerator = new CallbackGenerator( onInit: (i) => { }, onExecute: (e) => { cts.Cancel(); } ); // test generator cancels the token. Check that the call to this generator doesn't make it look like it errored. var testGenerator2 = new CallbackGenerator2( onInit: (i) => { }, onExecute: (e) => { e.AddSource("a", SourceText.From("public class E {}", Encoding.UTF8)); e.CancellationToken.ThrowIfCancellationRequested(); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator, testGenerator2 }, parseOptions: parseOptions); var oldDriver = driver; Assert.Throws<OperationCanceledException>(() => driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var outputDiagnostics, cts.Token) ); Assert.Same(oldDriver, driver); } [ConditionalFact(typeof(IsEnglishLocal))] public void Adding_A_Source_Text_Without_Encoding_Fails_Generation() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => { }, (sgc) => { sgc.AddSource("a", SourceText.From("")); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var outputDiagnostics); Assert.Single(outputDiagnostics); VerifyArgumentExceptionDiagnostic(outputDiagnostics.Single(), nameof(CallbackGenerator), "The SourceText with hintName 'a.cs' must have an explicit encoding set.", "source"); } [Fact] public void ParseOptions_Are_Passed_To_Generator() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); ParseOptions? passedOptions = null; var testGenerator = new CallbackGenerator( onInit: (i) => { }, onExecute: (e) => { passedOptions = e.ParseOptions; } ); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); Assert.Same(parseOptions, passedOptions); } [Fact] public void AdditionalFiles_Are_Passed_To_Generator() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var texts = ImmutableArray.Create<AdditionalText>(new InMemoryAdditionalText("a", "abc"), new InMemoryAdditionalText("b", "def")); ImmutableArray<AdditionalText> passedIn = default; var testGenerator = new CallbackGenerator( onInit: (i) => { }, onExecute: (e) => passedIn = e.AdditionalFiles ); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions, additionalTexts: texts); driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); Assert.Equal(2, passedIn.Length); Assert.Equal<AdditionalText>(texts, passedIn); } [Fact] public void AnalyzerConfigOptions_Are_Passed_To_Generator() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var options = new CompilerAnalyzerConfigOptionsProvider(ImmutableDictionary<object, AnalyzerConfigOptions>.Empty, new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty.Add("a", "abc").Add("b", "def"))); AnalyzerConfigOptionsProvider? passedIn = null; var testGenerator = new CallbackGenerator( onInit: (i) => { }, onExecute: (e) => passedIn = e.AnalyzerConfigOptions ); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { testGenerator }, parseOptions: parseOptions, optionsProvider: options); driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); Assert.NotNull(passedIn); Assert.True(passedIn!.GlobalOptions.TryGetValue("a", out var item1)); Assert.Equal("abc", item1); Assert.True(passedIn!.GlobalOptions.TryGetValue("b", out var item2)); Assert.Equal("def", item2); } [Fact] public void Generator_Can_Provide_Source_In_PostInit() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); static void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); } var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(postInit), (sgc) => { }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); outputCompilation.VerifyDiagnostics(); Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); } [Fact] public void PostInit_Source_Is_Available_During_Execute() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); static void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); } INamedTypeSymbol? dSymbol = null; var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(postInit), (sgc) => { dSymbol = sgc.Compilation.GetTypeByMetadataName("D"); }, source = "public class E : D {}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); outputCompilation.VerifyDiagnostics(); Assert.NotNull(dSymbol); } [Fact] public void PostInit_Source_Is_Available_To_Other_Generators_During_Execute() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); static void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); } INamedTypeSymbol? dSymbol = null; var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(postInit), (sgc) => { }); var generator2 = new CallbackGenerator2((ic) => { }, (sgc) => { dSymbol = sgc.Compilation.GetTypeByMetadataName("D"); }, source = "public class E : D {}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); outputCompilation.VerifyDiagnostics(); Assert.NotNull(dSymbol); } [Fact] public void PostInit_Is_Only_Called_Once() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int postInitCount = 0; int executeCount = 0; void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); postInitCount++; } var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(postInit), (sgc) => executeCount++, source = "public class E : D {}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); outputCompilation.VerifyDiagnostics(); Assert.Equal(1, postInitCount); Assert.Equal(3, executeCount); } [Fact] public void Error_During_PostInit_Is_Reported() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); static void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); throw new InvalidOperationException("post init error"); } var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(postInit), (sgc) => Assert.Fail("Should not execute"), source = "public class E : D {}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "post init error", initialization: true); } [Fact] public void Error_During_Initialization_PostInit_Does_Not_Run() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); static void init(GeneratorInitializationContext context) { context.RegisterForPostInitialization(postInit); throw new InvalidOperationException("init error"); } static void postInit(GeneratorPostInitializationContext context) { context.AddSource("postInit", "public class D {} "); Assert.Fail("Should not execute"); } var generator = new CallbackGenerator(init, (sgc) => Assert.Fail("Should not execute"), source = "public class E : D {}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(generatorDiagnostics.Single(), nameof(CallbackGenerator), "init error", initialization: true); } [Fact] public void PostInit_SyntaxTrees_Are_Available_In_RunResults() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(pic => pic.AddSource("postInit", "public class D{}")), (sgc) => { }, "public class E{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Single(results.Results); Assert.Empty(results.Diagnostics); var result = results.Results[0]; Assert.Null(result.Exception); Assert.Empty(result.Diagnostics); Assert.Equal(2, result.GeneratedSources.Length); } [Fact] public void PostInit_SyntaxTrees_Are_Combined_In_RunResults() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new CallbackGenerator((ic) => ic.RegisterForPostInitialization(pic => pic.AddSource("postInit", "public class D{}")), (sgc) => { }, "public class E{}"); var generator2 = new SingleFileTestGenerator("public class F{}"); var generator3 = new SingleFileTestGenerator2("public class G{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator, generator2, generator3 }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Equal(4, results.GeneratedTrees.Length); Assert.Equal(3, results.Results.Length); Assert.Empty(results.Diagnostics); var result1 = results.Results[0]; var result2 = results.Results[1]; var result3 = results.Results[2]; Assert.Null(result1.Exception); Assert.Empty(result1.Diagnostics); Assert.Equal(2, result1.GeneratedSources.Length); Assert.Equal(results.GeneratedTrees[0], result1.GeneratedSources[0].SyntaxTree); Assert.Equal(results.GeneratedTrees[1], result1.GeneratedSources[1].SyntaxTree); Assert.Null(result2.Exception); Assert.Empty(result2.Diagnostics); Assert.Single(result2.GeneratedSources); Assert.Equal(results.GeneratedTrees[2], result2.GeneratedSources[0].SyntaxTree); Assert.Null(result3.Exception); Assert.Empty(result3.Diagnostics); Assert.Single(result3.GeneratedSources); Assert.Equal(results.GeneratedTrees[3], result3.GeneratedSources[0].SyntaxTree); } [Fact] public void SyntaxTrees_Are_Lazy() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new SingleFileTestGenerator("public class D {}", "source.cs"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); var tree = Assert.Single(results.GeneratedTrees); Assert.False(tree.TryGetRoot(out _)); var rootFromGetRoot = tree.GetRoot(); Assert.NotNull(rootFromGetRoot); Assert.True(tree.TryGetRoot(out var rootFromTryGetRoot)); Assert.Same(rootFromGetRoot, rootFromTryGetRoot); } [Fact] public void Diagnostics_Respect_Suppression() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); CallbackGenerator gen = new CallbackGenerator((c) => { }, (c) => { c.ReportDiagnostic(CSDiagnostic.Create("GEN001", "generators", "message", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 2)); c.ReportDiagnostic(CSDiagnostic.Create("GEN002", "generators", "message", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 3)); }); var options = ((CSharpCompilationOptions)compilation.Options); // generator driver diagnostics are reported separately from the compilation verifyDiagnosticsWithOptions(options, Diagnostic("GEN001").WithLocation(1, 1), Diagnostic("GEN002").WithLocation(1, 1)); // warnings can be individually suppressed verifyDiagnosticsWithOptions(options.WithSpecificDiagnosticOptions("GEN001", ReportDiagnostic.Suppress), Diagnostic("GEN002").WithLocation(1, 1)); verifyDiagnosticsWithOptions(options.WithSpecificDiagnosticOptions("GEN002", ReportDiagnostic.Suppress), Diagnostic("GEN001").WithLocation(1, 1)); // warning level is respected verifyDiagnosticsWithOptions(options.WithWarningLevel(0)); verifyDiagnosticsWithOptions(options.WithWarningLevel(2), Diagnostic("GEN001").WithLocation(1, 1)); verifyDiagnosticsWithOptions(options.WithWarningLevel(3), Diagnostic("GEN001").WithLocation(1, 1), Diagnostic("GEN002").WithLocation(1, 1)); // warnings can be upgraded to errors verifyDiagnosticsWithOptions(options.WithSpecificDiagnosticOptions("GEN001", ReportDiagnostic.Error), Diagnostic("GEN001").WithLocation(1, 1).WithWarningAsError(true), Diagnostic("GEN002").WithLocation(1, 1)); verifyDiagnosticsWithOptions(options.WithSpecificDiagnosticOptions("GEN002", ReportDiagnostic.Error), Diagnostic("GEN001").WithLocation(1, 1), Diagnostic("GEN002").WithLocation(1, 1).WithWarningAsError(true)); void verifyDiagnosticsWithOptions(CompilationOptions options, params DiagnosticDescription[] expected) { GeneratorDriver driver = CSharpGeneratorDriver.Create(ImmutableArray.Create(gen), parseOptions: parseOptions); var updatedCompilation = compilation.WithOptions(options); driver.RunGeneratorsAndUpdateCompilation(updatedCompilation, out var outputCompilation, out var diagnostics); outputCompilation.VerifyDiagnostics(); diagnostics.Verify(expected); } } [Fact] public void Diagnostics_Respect_Pragma_Suppression() { var gen001 = CSDiagnostic.Create("GEN001", "generators", "message", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 2); // reported diagnostics can have a location in source verifyDiagnosticsWithSource("//comment", new[] { (gen001, TextSpan.FromBounds(2, 5)) }, Diagnostic("GEN001", "com").WithLocation(1, 3)); // diagnostics are suppressed via #pragma verifyDiagnosticsWithSource( @"#pragma warning disable //comment", new[] { (gen001, TextSpan.FromBounds(27, 30)) }, Diagnostic("GEN001", "com", isSuppressed: true).WithLocation(2, 3)); // but not when they don't have a source location verifyDiagnosticsWithSource( @"#pragma warning disable //comment", new[] { (gen001, new TextSpan(0, 0)) }, Diagnostic("GEN001").WithLocation(1, 1)); // can be suppressed explicitly verifyDiagnosticsWithSource( @"#pragma warning disable GEN001 //comment", new[] { (gen001, TextSpan.FromBounds(34, 37)) }, Diagnostic("GEN001", "com", isSuppressed: true).WithLocation(2, 3)); // suppress + restore verifyDiagnosticsWithSource( @"#pragma warning disable GEN001 //comment #pragma warning restore GEN001 //another", new[] { (gen001, TextSpan.FromBounds(34, 37)), (gen001, TextSpan.FromBounds(77, 80)) }, Diagnostic("GEN001", "com", isSuppressed: true).WithLocation(2, 3), Diagnostic("GEN001", "ano").WithLocation(4, 3)); void verifyDiagnosticsWithSource(string source, (Diagnostic, TextSpan)[] reportDiagnostics, params DiagnosticDescription[] expected) { var parseOptions = TestOptions.Regular; source = source.Replace(Environment.NewLine, "\r\n"); Compilation compilation = CreateCompilation(source, sourceFileName: "sourcefile.cs", options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); CallbackGenerator gen = new CallbackGenerator((c) => { }, (c) => { foreach ((var d, var l) in reportDiagnostics) { if (l.IsEmpty) { c.ReportDiagnostic(d); } else { c.ReportDiagnostic(d.WithLocation(Location.Create(c.Compilation.SyntaxTrees.First(), l))); } } }); GeneratorDriver driver = CSharpGeneratorDriver.Create(ImmutableArray.Create(gen), parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); outputCompilation.VerifyDiagnostics(); diagnostics.Verify(expected); } } [Fact, WorkItem(66337, "https://github.com/dotnet/roslyn/issues/66337")] public void Diagnostics_Respect_SuppressMessageAttribute() { var gen001 = CSDiagnostic.Create("GEN001", "generators", "message", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 2); // reported diagnostics can have a location in source verify(""" class C { //comment } """, new[] { (gen001, "com") }, Diagnostic("GEN001", "com").WithLocation(3, 7)); // diagnostics are suppressed via SuppressMessageAttribute verify(""" [System.Diagnostics.CodeAnalysis.SuppressMessage("", "GEN001")] class C { //comment } """, new[] { (gen001, "com") }, Diagnostic("GEN001", "com", isSuppressed: true).WithLocation(4, 7)); // but not when they don't have a source location verify(""" [System.Diagnostics.CodeAnalysis.SuppressMessage("", "GEN001")] class C { //comment } """, new[] { (gen001, "") }, Diagnostic("GEN001").WithLocation(1, 1)); // different ID suppressed + multiple diagnostics verify(""" [System.Diagnostics.CodeAnalysis.SuppressMessage("", "GEN002")] class C { //comment //another } """, new[] { (gen001, "com"), (gen001, "ano") }, Diagnostic("GEN001", "com").WithLocation(4, 7), Diagnostic("GEN001", "ano").WithLocation(5, 7)); // diagnostics are suppressed via SuppressMessageAttribute on a primary constructor verify(""" [method: System.Diagnostics.CodeAnalysis.SuppressMessage("", "GEN001")] class C(int i) { public int I { get; } = i; } """, new[] { (gen001, "int") }, Diagnostic("GEN001", "int", isSuppressed: true).WithLocation(2, 9)); static void verify(string source, IReadOnlyList<(Diagnostic Diagnostic, string Location)> reportDiagnostics, params DiagnosticDescription[] expected) { var parseOptions = TestOptions.RegularPreview; source = source.Replace(Environment.NewLine, "\r\n"); var compilation = CreateCompilation(source, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var syntaxTree = compilation.SyntaxTrees.Single(); var actualDiagnostics = reportDiagnostics.SelectAsArray(x => { if (string.IsNullOrEmpty(x.Location)) { return x.Diagnostic; } var start = source.IndexOf(x.Location); Assert.True(start >= 0, $"Not found in source: '{x.Location}'"); var end = start + x.Location.Length; return x.Diagnostic.WithLocation(Location.Create(syntaxTree, TextSpan.FromBounds(start, end))); }); var gen = new CallbackGenerator(c => { }, c => { foreach (var d in actualDiagnostics) { c.ReportDiagnostic(d); } }); var driver = CSharpGeneratorDriver.Create(new[] { gen }, parseOptions: parseOptions); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); outputCompilation.VerifyDiagnostics(); diagnostics.Verify(expected); } } [Fact] public void GeneratorDriver_Prefers_Incremental_Generators() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int initCount = 0, executeCount = 0; var generator = new CallbackGenerator((ic) => initCount++, (sgc) => executeCount++); int incrementalInitCount = 0; var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ic) => incrementalInitCount++)); int dualInitCount = 0, dualExecuteCount = 0, dualIncrementalInitCount = 0; var generator3 = new IncrementalAndSourceCallbackGenerator((ic) => dualInitCount++, (sgc) => dualExecuteCount++, (ic) => dualIncrementalInitCount++); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator, generator2, generator3 }, parseOptions: parseOptions); driver.RunGenerators(compilation); // ran individual incremental and source generators Assert.Equal(1, initCount); Assert.Equal(1, executeCount); Assert.Equal(1, incrementalInitCount); // ran the combined generator only as an IIncrementalGenerator Assert.Equal(0, dualInitCount); Assert.Equal(0, dualExecuteCount); Assert.Equal(1, dualIncrementalInitCount); } [Fact] public void GeneratorDriver_Initializes_Incremental_Generators() { var source = @" class C { } "; var parseOptions = TestOptions.Regular; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); int incrementalInitCount = 0; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ic) => incrementalInitCount++)); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver.RunGenerators(compilation); // ran the incremental generator Assert.Equal(1, incrementalInitCount); } [Fact] public void Incremental_Generators_Exception_During_Initialization() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var e = new InvalidOperationException("abc"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ic) => throw e)); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Single(runResults.Diagnostics); Assert.Single(runResults.Results); Assert.Empty(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results[0].Exception); } [Fact] public void Incremental_Generators_Exception_During_Execution() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var e = new InvalidOperationException("abc"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => throw e))); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Single(runResults.Diagnostics); Assert.Single(runResults.Results); Assert.Empty(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results[0].Exception); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67386")] public void Incremental_Generators_Exception_In_Comparer() { var source = """ class Attr : System.Attribute { } [Attr] class C { } """; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var syntaxTree = compilation.SyntaxTrees.Single(); var e = new InvalidOperationException("abc"); var generator = new PipelineCallbackGenerator((ctx) => { var name = ctx.ForAttributeWithSimpleName<ClassDeclarationSyntax>("Attr") .Select((c, _) => c.Identifier.ValueText) .WithComparer(new LambdaComparer<string>((_, _) => throw e)); ctx.RegisterSourceOutput(name, (spc, n) => spc.AddSource(n, "// generated")); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Empty(runResults.Diagnostics); Assert.Equal("// generated", runResults.Results.Single().GeneratedSources.Single().SourceText.ToString()); compilation = compilation.ReplaceSyntaxTree(syntaxTree, CSharpSyntaxTree.ParseText(""" class Attr : System.Attribute { } [Attr] class D { } """, parseOptions)); compilation.VerifyDiagnostics(); driver = driver.RunGenerators(compilation); runResults = driver.GetRunResult(); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(runResults.Diagnostics.Single(), nameof(PipelineCallbackGenerator), "abc"); Assert.Empty(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results.Single().Exception); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76765")] public void Incremental_Generators_Exception_In_DefaultComparer() { var source = """ class C { } """; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var syntaxTree = compilation.SyntaxTrees.Single(); var e = new InvalidOperationException("abc"); var generator = new PipelineCallbackGenerator((ctx) => { var name = ctx.CompilationProvider.Select((c, _) => new ThrowWhenEqualsItem(e)); ctx.RegisterSourceOutput(name, (spc, n) => spc.AddSource("item.cs", "// generated")); }); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Empty(runResults.Diagnostics); Assert.Equal("// generated", runResults.Results.Single().GeneratedSources.Single().SourceText.ToString()); compilation = compilation.ReplaceSyntaxTree(syntaxTree, CSharpSyntaxTree.ParseText(""" class D { } """, parseOptions)); compilation.VerifyDiagnostics(); driver = driver.RunGenerators(compilation); runResults = driver.GetRunResult(); VerifyGeneratorExceptionDiagnostic<InvalidOperationException>(runResults.Diagnostics.Single(), nameof(PipelineCallbackGenerator), "abc"); Assert.Empty(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results.Single().Exception); } class ThrowWhenEqualsItem(Exception toThrow) { readonly Exception _toThrow = toThrow; public override bool Equals(object? obj) => throw _toThrow; public override int GetHashCode() => throw new NotImplementedException(); } [Fact] public void Incremental_Generators_Exception_During_Execution_Doesnt_Produce_AnySource() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var e = new InvalidOperationException("abc"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => spc.AddSource("test", "")); ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => throw e); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Single(runResults.Diagnostics); Assert.Single(runResults.Results); Assert.Empty(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results[0].Exception); } [Fact] public void Incremental_Generators_Exception_During_Execution_Doesnt_Stop_Other_Generators() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var e = new InvalidOperationException("abc"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => throw e); })); var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => spc.AddSource("test", "")); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator, generator2 }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResults = driver.GetRunResult(); Assert.Single(runResults.Diagnostics); Assert.Equal(2, runResults.Results.Length); Assert.Single(runResults.GeneratedTrees); Assert.Equal(e, runResults.Results[0].Exception); } [Fact] public void IncrementalGenerator_With_No_Pipeline_Callback_Is_Valid() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ic) => { })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); outputCompilation.VerifyDiagnostics(); Assert.Empty(diagnostics); } [Fact] public void IncrementalGenerator_Can_Add_PostInit_Source() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ic) => ic.RegisterPostInitializationOutput(c => c.AddSource("a", "class D {}")))); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); Assert.Equal(2, outputCompilation.SyntaxTrees.Count()); Assert.Empty(diagnostics); } [Fact] public void User_WrappedFunc_Throw_Exceptions() { Func<int, CancellationToken, int> func = (input, _) => input; Func<int, CancellationToken, int> throwsFunc = (input, _) => throw new InvalidOperationException("user code exception"); Func<int, CancellationToken, int> timeoutFunc = (input, ct) => { ct.ThrowIfCancellationRequested(); return input; }; Func<int, CancellationToken, int> otherTimeoutFunc = (input, _) => throw new OperationCanceledException(); var userFunc = func.WrapUserFunction(catchAnalyzerExceptions: true); var userThrowsFunc = throwsFunc.WrapUserFunction(catchAnalyzerExceptions: true); var userTimeoutFunc = timeoutFunc.WrapUserFunction(catchAnalyzerExceptions: true); var userOtherTimeoutFunc = otherTimeoutFunc.WrapUserFunction(catchAnalyzerExceptions: true); // user functions return same values when wrapped var result = userFunc(10, CancellationToken.None); var userResult = userFunc(10, CancellationToken.None); Assert.Equal(10, result); Assert.Equal(result, userResult); // exceptions thrown in user code are wrapped Assert.Throws<InvalidOperationException>(() => throwsFunc(20, CancellationToken.None)); Assert.Throws<UserFunctionException>(() => userThrowsFunc(20, CancellationToken.None)); try { userThrowsFunc(20, CancellationToken.None); } catch (UserFunctionException e) { Assert.IsType<InvalidOperationException>(e.InnerException); } // cancellation is not wrapped, and is bubbled up Assert.Throws<OperationCanceledException>(() => timeoutFunc(30, new CancellationToken(true))); Assert.Throws<OperationCanceledException>(() => userTimeoutFunc(30, new CancellationToken(true))); // unless it wasn't *our* cancellation token, in which case it still gets wrapped Assert.Throws<OperationCanceledException>(() => otherTimeoutFunc(30, CancellationToken.None)); Assert.Throws<UserFunctionException>(() => userOtherTimeoutFunc(30, CancellationToken.None)); } [Fact] public void IncrementalGenerator_Doesnt_Run_For_Same_Input() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider.Select((c, ct) => c).WithTrackingName("IdentityTransform"), (spc, c) => { }); })); // run the generator once, and check it was passed the compilation GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["IdentityTransform"], step => { Assert.Collection(step.Inputs, source => Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason)); Assert.Collection(step.Outputs, output => Assert.Equal(IncrementalStepRunReason.New, output.Reason)); }); // run the same compilation through again, and confirm the output wasn't called driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["IdentityTransform"], step => { Assert.Collection(step.Inputs, source => Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason)); Assert.Collection(step.Outputs, output => Assert.Equal(IncrementalStepRunReason.Cached, output.Reason)); }); } [Fact] public void IncrementalGenerator_Runs_Only_For_Changed_Inputs() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var text1 = new InMemoryAdditionalText("Text1", "content1"); var text2 = new InMemoryAdditionalText("Text2", "content2"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider.Select((c, ct) => c).WithTrackingName("CompilationTransform"), (spc, c) => { }); ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider.Select((at, ct) => at).WithTrackingName("AdditionalTextsTransform"), (spc, at) => { }); })); // run the generator once, and check it was passed the compilation GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, additionalTexts: new[] { text1 }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["CompilationTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(compilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(compilation, output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["AdditionalTextsTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text1, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text1, output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // add an additional text, but keep the compilation the same driver = driver.AddAdditionalTexts(ImmutableArray.Create<AdditionalText>(text2)); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["CompilationTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(compilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(compilation, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["AdditionalTextsTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text1, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text1, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text2, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text2, output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // now edit the compilation var newCompilation = compilation.WithOptions(compilation.Options.WithModuleName("newComp")); driver = driver.RunGenerators(newCompilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["CompilationTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(newCompilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Modified, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(newCompilation, output.Value); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["AdditionalTextsTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text1, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text1, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text2, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text2, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // re run without changing anything driver = driver.RunGenerators(newCompilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["CompilationTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(newCompilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(newCompilation, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["AdditionalTextsTransform"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text1, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text1, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, source => { Assert.Equal(text2, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal(text2, output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); } [Fact] public void IncrementalGenerator_Can_Add_Comparer_To_Input_Node() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); List<Compilation> compilationsCalledFor = new List<Compilation>(); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var compilationSource = ctx.CompilationProvider.WithComparer(new LambdaComparer<Compilation>((c1, c2) => true, 0)); ctx.RegisterSourceOutput(compilationSource, (spc, c) => { compilationsCalledFor.Add(c); }); })); // run the generator once, and check it was passed the compilation GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); Assert.Equal(1, compilationsCalledFor.Count); Assert.Equal(compilation, compilationsCalledFor[0]); // now edit the compilation, run the generator, and confirm that the output was not called again this time Compilation newCompilation = compilation.WithOptions(compilation.Options.WithModuleName("newCompilation")); driver = driver.RunGenerators(newCompilation); Assert.Equal(1, compilationsCalledFor.Count); Assert.Equal(compilation, compilationsCalledFor[0]); } [Fact] public void IncrementalGenerator_Can_Add_Comparer_To_Combine_Node() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); List<AdditionalText> texts = new List<AdditionalText>() { new InMemoryAdditionalText("abc", "") }; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var compilationSource = ctx.CompilationProvider.Combine(ctx.AdditionalTextsProvider.Collect()) // comparer that ignores the LHS (additional texts) .WithComparer(new LambdaComparer<(Compilation, ImmutableArray<AdditionalText>)>((c1, c2) => c1.Item1 == c2.Item1, 0)) .WithTrackingName("Step") .Select((x, ct) => x) .WithTrackingName("Step2"); ctx.RegisterSourceOutput(compilationSource, (spc, c) => { }); })); // run the generator once, and check it was passed the compilation + additional texts GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: texts, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Step"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(compilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason); }, source => { Assert.Equal(texts[0], ((ImmutableArray<AdditionalText>)source.Source.Outputs[source.OutputIndex].Value)[0]); Assert.Equal(IncrementalStepRunReason.New, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { var value = ((Compilation, ImmutableArray<AdditionalText>))output.Value; Assert.Equal(compilation, value.Item1); Assert.Equal(texts[0], value.Item2.Single()); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // edit the additional texts, and verify that the step output is considered "unchanged" and that the value is the same as the previous value. driver = driver.RemoveAdditionalTexts(texts.ToImmutableArray()); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Step"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(compilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Cached, source.Source.Outputs[source.OutputIndex].Reason); }, source => { Assert.Empty((ImmutableArray<AdditionalText>)source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Modified, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { var value = ((Compilation, ImmutableArray<AdditionalText>))output.Value; Assert.Equal(compilation, value.Item1); Assert.Equal(texts[0], value.Item2.Single()); Assert.Equal(IncrementalStepRunReason.Unchanged, output.Reason); }); }); // Verify that a step that consumes the result of the Combine step gets the old value as an input // and considers the value cached. Assert.Collection(runResult.TrackedSteps["Step2"], step => { Assert.Collection(step.Inputs, source => { var value = ((Compilation, ImmutableArray<AdditionalText>))source.Source.Outputs[source.OutputIndex].Value; Assert.Equal(compilation, value.Item1); Assert.Equal(texts[0], value.Item2.Single()); Assert.Equal(IncrementalStepRunReason.Unchanged, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { var value = ((Compilation, ImmutableArray<AdditionalText>))output.Value; Assert.Equal(compilation, value.Item1); Assert.Equal(texts[0], value.Item2.Single()); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // now edit the compilation, run the generator, and confirm that the output *was* called again this time with the new compilation and no additional texts Compilation newCompilation = compilation.WithOptions(compilation.Options.WithModuleName("newCompilation")); driver = driver.RunGenerators(newCompilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Step"], step => { Assert.Collection(step.Inputs, source => { Assert.Equal(newCompilation, source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Modified, source.Source.Outputs[source.OutputIndex].Reason); }, source => { Assert.Empty((ImmutableArray<AdditionalText>)source.Source.Outputs[source.OutputIndex].Value); Assert.Equal(IncrementalStepRunReason.Unchanged, source.Source.Outputs[source.OutputIndex].Reason); }); Assert.Collection(step.Outputs, output => { var value = ((Compilation, ImmutableArray<AdditionalText>))output.Value; Assert.Equal(newCompilation, value.Item1); Assert.Empty(value.Item2); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }); } [Fact, WorkItem(61162, "https://github.com/dotnet/roslyn/issues/61162")] public void IncrementalGenerator_Collect_SyntaxProvider_01() { var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(static ctx => { var invokedMethodsProvider = ctx.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is InvocationExpressionSyntax, static (ctx, ct) => ctx.SemanticModel.GetSymbolInfo(ctx.Node, ct).Symbol?.Name ?? "(method not found)") .Collect(); ctx.RegisterSourceOutput(invokedMethodsProvider, static (spc, invokedMethods) => { spc.AddSource("InvokedMethods.g.cs", string.Join(Environment.NewLine, invokedMethods.Select(m => $"// {m}"))); }); })); var source = """ System.Console.WriteLine(); System.Console.WriteLine(); System.Console.WriteLine(); System.Console.WriteLine(); """; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugExeThrowing, parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); verify(ref driver, compilation, """ // WriteLine // WriteLine // WriteLine // WriteLine """); replace(ref compilation, parseOptions, """ System.Console.WriteLine(); System.Console.WriteLine(); """); verify(ref driver, compilation, """ // WriteLine // WriteLine """); replace(ref compilation, parseOptions, "_ = 0;"); verify(ref driver, compilation, ""); static void verify(ref GeneratorDriver driver, Compilation compilation, string generatedContent) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); var generatedTree = driver.GetRunResult().GeneratedTrees.Single(); AssertEx.EqualOrDiff(generatedContent, generatedTree.ToString()); } static void replace(ref Compilation compilation, CSharpParseOptions parseOptions, string source) { compilation = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.Single(), CSharpSyntaxTree.ParseText(source, parseOptions)); } } [Fact, WorkItem(61162, "https://github.com/dotnet/roslyn/issues/61162")] public void IncrementalGenerator_Collect_SyntaxProvider_02() { var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(static ctx => { var invokedMethodsProvider = ctx.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is InvocationExpressionSyntax, static (ctx, ct) => ctx.SemanticModel.GetSymbolInfo(ctx.Node, ct).Symbol?.Name ?? "(method not found)") .Select((n, _) => n); ctx.RegisterSourceOutput(invokedMethodsProvider, static (spc, invokedMethod) => { spc.AddSource(invokedMethod, "// " + invokedMethod); }); })); var source = """ System.Console.WriteLine(); System.Console.ReadLine(); """; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugExeThrowing, parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); verify(ref driver, compilation, new[] { "// WriteLine", "// ReadLine" }); replace(ref compilation, parseOptions, """ System.Console.WriteLine(); """); verify(ref driver, compilation, new[] { "// WriteLine" }); replace(ref compilation, parseOptions, "_ = 0;"); verify(ref driver, compilation, Array.Empty<string>()); static void verify(ref GeneratorDriver driver, Compilation compilation, string[] generatedContent) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); var trees = driver.GetRunResult().GeneratedTrees; Assert.Equal(generatedContent.Length, trees.Length); for (int i = 0; i < generatedContent.Length; i++) { AssertEx.EqualOrDiff(generatedContent[i], trees[i].ToString()); } } static void replace(ref Compilation compilation, CSharpParseOptions parseOptions, string source) { compilation = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.Single(), CSharpSyntaxTree.ParseText(source, parseOptions)); } } [Fact] public void IncrementalGenerator_Register_End_Node_Only_Once_Through_Combines() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); List<Compilation> compilationsCalledFor = new List<Compilation>(); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var source = ctx.CompilationProvider; var source2 = ctx.CompilationProvider.Combine(source); var source3 = ctx.CompilationProvider.Combine(source2); var source4 = ctx.CompilationProvider.Combine(source3); var source5 = ctx.CompilationProvider.Combine(source4); ctx.RegisterSourceOutput(source5, (spc, c) => { compilationsCalledFor.Add(c.Item1); }); })); // run the generator and check that we didn't multiple register the generate source node through the combine GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); Assert.Equal(1, compilationsCalledFor.Count); Assert.Equal(compilation, compilationsCalledFor[0]); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67633")] public void IncrementalGenerator_SyntaxProvider_InputRemoved() { var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(static ctx => { var invokedMethodsProvider = ctx.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is InvocationExpressionSyntax, static (ctx, ct) => ctx.SemanticModel.GetSymbolInfo(ctx.Node, ct).Symbol?.Name ?? "(method not found)") .Select((n, _) => n) .WithTrackingName("Select"); ctx.RegisterSourceOutput(invokedMethodsProvider, static (spc, invokedMethod) => { spc.AddSource(invokedMethod, "// " + invokedMethod); }); })); var source1 = """ System.Console.WriteLine(); System.Console.ReadLine(); """; var source2 = """ class C { public void M() { System.Console.Clear(); System.Console.Beep(); } } """; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(new[] { source1, source2 }, options: TestOptions.DebugExeThrowing, parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); verify(ref driver, compilation, new[] { "// WriteLine", "// ReadLine", "// Clear", "// Beep" }); // edit part of source 1 replace(ref compilation, parseOptions, """ System.Console.WriteLine(); System.Console.Write(' '); """); verify(ref driver, compilation, new[] { "// WriteLine", "// Write", "// Clear", "// Beep" }); Assert.Equal(new (object, IncrementalStepRunReason)[] { ("WriteLine", IncrementalStepRunReason.Cached), ("Write", IncrementalStepRunReason.Modified), ("Clear", IncrementalStepRunReason.Cached), ("Beep", IncrementalStepRunReason.Cached) }, driver.GetRunResult().Results.Single().TrackedSteps["Select"].Select(r => r.Outputs.Single())); // remove second line of source 1 replace(ref compilation, parseOptions, """ System.Console.WriteLine(); """); verify(ref driver, compilation, new[] { "// WriteLine", "// Clear", "// Beep" }); Assert.Equal(new (object, IncrementalStepRunReason)[] { ("WriteLine", IncrementalStepRunReason.Cached), ("Write", IncrementalStepRunReason.Removed), ("Clear", IncrementalStepRunReason.Cached), ("Beep", IncrementalStepRunReason.Cached) }, driver.GetRunResult().Results.Single().TrackedSteps["Select"].Select(r => r.Outputs.Single())); static void verify(ref GeneratorDriver driver, Compilation compilation, string[] generatedContent) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); var trees = driver.GetRunResult().GeneratedTrees; Assert.Equal(generatedContent.Length, trees.Length); for (int i = 0; i < generatedContent.Length; i++) { AssertEx.EqualOrDiff(generatedContent[i], trees[i].ToString()); } } static void replace(ref Compilation compilation, CSharpParseOptions parseOptions, string source) { compilation = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), CSharpSyntaxTree.ParseText(source, parseOptions)); } } [Fact] public void IncrementalGenerator_PostInit_Source_Is_Cached() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => { ctx.RegisterPostInitializationOutput(c => c.AddSource("a", "class D {}")); var input = ctx.SyntaxProvider.CreateSyntaxProvider(static (n, _) => n is ClassDeclarationSyntax, (gsc, _) => (ClassDeclarationSyntax)gsc.Node) .Select((c, ct) => c).WithTrackingName("Classes"); ctx.RegisterSourceOutput(input, (spc, node) => { }); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Classes"], step => { Assert.Equal("C", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.New, step.Outputs[0].Reason); }, step => { Assert.Equal("D", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.New, step.Outputs[0].Reason); }); // re-run without changes driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Classes"], step => { Assert.Equal("C", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }, step => { Assert.Equal("D", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); // modify the original tree, see that the post init is still cached var c2 = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), CSharpSyntaxTree.ParseText("class E{}", parseOptions)); driver = driver.RunGenerators(c2); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Classes"], step => { Assert.Equal("E", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.Modified, step.Outputs[0].Reason); }, step => { Assert.Equal("D", ((ClassDeclarationSyntax)step.Outputs[0].Value).Identifier.ValueText); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); } [Fact] public void IncrementalGenerator_PostInit_AddEmbeddedAttributeSource_Adds() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var callback = (IncrementalGeneratorInitializationContext ctx) => ctx.RegisterPostInitializationOutput(c => c.AddEmbeddedAttributeDefinition()); var generator1 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(callback)); var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(callback)); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1, generator2], parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); var results = driver.GetRunResult().Results; Assert.Equal(2, results.Length); foreach (var runResult in results) { Assert.Single(runResult.GeneratedSources); var generatedSource = runResult.GeneratedSources[0]; Assert.Equal(""" // <auto-generated/> namespace Microsoft.CodeAnalysis { internal sealed partial class EmbeddedAttribute : global::System.Attribute { } } """, generatedSource.SourceText.ToString()); Assert.Equal("Microsoft.CodeAnalysis.EmbeddedAttribute.cs", generatedSource.HintName); } outputCompilation.VerifyDiagnostics(); } [Fact] public void IncrementalGenerator_PostInit_AddEmbeddedAttributeSource_DoubleAdd_Throws() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => { ctx.RegisterPostInitializationOutput(c => { c.AddEmbeddedAttributeDefinition(); Assert.Throws<ArgumentException>("hintName", () => c.AddEmbeddedAttributeDefinition()); }); })); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Single(runResult.GeneratedSources); var generatedSource = runResult.GeneratedSources[0]; Assert.Equal(""" // <auto-generated/> namespace Microsoft.CodeAnalysis { internal sealed partial class EmbeddedAttribute : global::System.Attribute { } } """, generatedSource.SourceText.ToString()); Assert.Equal("Microsoft.CodeAnalysis.EmbeddedAttribute.cs", generatedSource.HintName); } [Fact] public void Incremental_Generators_Can_Be_Cancelled() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); CancellationTokenSource cts = new CancellationTokenSource(); bool generatorCancelled = false; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) => { var step1 = ctx.CompilationProvider.Select((c, ct) => { generatorCancelled = true; cts.Cancel(); return c; }); var step2 = step1.Select((c, ct) => { ct.ThrowIfCancellationRequested(); return c; }); ctx.RegisterSourceOutput(step2, (spc, c) => spc.AddSource("a", "")); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); Assert.Throws<OperationCanceledException>(() => driver = driver.RunGenerators(compilation, cancellationToken: cts.Token)); Assert.True(generatorCancelled); } [Fact] public void ParseOptions_Can_Be_Updated() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, p) => { }); })); // run the generator once, and check it was passed the parse options GeneratorDriver driver = CSharpGeneratorDriver.Create( [generator], parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); GeneratorRunResult runResult = driver.GetRunResult().Results[0]; Assert.Single(runResult.TrackedSteps["ParseOptions"]); var output = runResult.TrackedSteps["ParseOptions"][0].Outputs[0].Value; Assert.Equal(parseOptions, output); // re-run without changes driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["ParseOptions"], step => { Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); // now update the parse options var newParseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Diagnose); driver = driver.WithUpdatedParseOptions(newParseOptions); // check we ran driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Single(runResult.TrackedSteps["ParseOptions"]); output = runResult.TrackedSteps["ParseOptions"][0].Outputs[0].Value; Assert.Equal(newParseOptions, output); // re-run without changes driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["ParseOptions"], step => { Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); // replace it with null, and check that it throws Assert.Throws<ArgumentNullException>(() => driver.WithUpdatedParseOptions(null!)); } [Fact, WorkItem(57455, "https://github.com/dotnet/roslyn/issues/57455")] public void RemoveTriggeringSyntaxAndVerifySyntaxTreeConsistentWithCompilation() { var source = @" [System.Obsolete] class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations = ctx.SyntaxProvider .CreateSyntaxProvider(static (s, t) => isSyntaxTargetForGeneration(s), static (context, ct) => getSemanticTargetForGeneration(context, ct)) .Where(static c => c is not null)!; IncrementalValueProvider<(Compilation, ImmutableArray<ClassDeclarationSyntax>)> compilationAndClasses = ctx.CompilationProvider.Combine(classDeclarations.Collect()); ctx.RegisterSourceOutput(compilationAndClasses, (context, ct) => validate(ct.Item1, ct.Item2)); })); // run the generator once GeneratorDriver driver = CSharpGeneratorDriver.Create( [generator], parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(disabledOutputs: IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); driver = driver.RunGenerators(compilation); Assert.True(driver.GetRunResult().Diagnostics.IsEmpty); // now update the source var newSource = @" class C { } "; Compilation newCompilation = CreateCompilation(newSource, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); // check we ran driver = driver.RunGenerators(newCompilation); Assert.True(driver.GetRunResult().Diagnostics.IsEmpty); return; static void validate(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> nodes) { foreach (var node in nodes) { Assert.True(compilation.SyntaxTrees.Contains(node.SyntaxTree)); } } static bool isSyntaxTargetForGeneration(SyntaxNode node) => node is ClassDeclarationSyntax { AttributeLists: { Count: > 0 } }; static ClassDeclarationSyntax? getSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken cancellationToken) { var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists) { return classDeclarationSyntax; } return null; } } [Fact] public void AnalyzerConfig_Can_Be_Updated() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.AnalyzerConfigOptionsProvider.Select((p, ct) => { p.GlobalOptions.TryGetValue("test", out var analyzerOptionsValue); return analyzerOptionsValue; }).WithTrackingName("AnalyzerConfig"), (spc, p) => { }); })); var builder = ImmutableDictionary<string, string>.Empty.ToBuilder(); builder.Add("test", "value1"); var optionsProvider = new CompilerAnalyzerConfigOptionsProvider(ImmutableDictionary<object, AnalyzerConfigOptions>.Empty, new DictionaryAnalyzerConfigOptions(builder.ToImmutable())); // run the generator once, and check it was passed the configs GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, optionsProvider: optionsProvider, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["AnalyzerConfig"], step => { Assert.Equal("AnalyzerConfig", step.Name); Assert.Collection(step.Outputs, output => { Assert.Equal("value1", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // re-run without changes. driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["AnalyzerConfig"], step => { Assert.Equal("AnalyzerConfig", step.Name); Assert.Collection(step.Outputs, output => { Assert.Equal("value1", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // now update the config builder.Clear(); builder.Add("test", "value2"); var newOptionsProvider = optionsProvider.WithGlobalOptions(new DictionaryAnalyzerConfigOptions(builder.ToImmutable())); driver = driver.WithUpdatedAnalyzerConfigOptions(newOptionsProvider); // check we ran driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["AnalyzerConfig"], step => { Assert.Equal("AnalyzerConfig", step.Name); Assert.Collection(step.Outputs, output => { Assert.Equal("value2", output.Value); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }); // replace it with null, and check that it throws Assert.Throws<ArgumentNullException>(() => driver.WithUpdatedAnalyzerConfigOptions(null!)); } [Fact] public void AdditionalText_Can_Be_Replaced() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); InMemoryAdditionalText additionalText1 = new InMemoryAdditionalText("path1.txt", ""); InMemoryAdditionalText additionalText2 = new InMemoryAdditionalText("path2.txt", ""); InMemoryAdditionalText additionalText3 = new InMemoryAdditionalText("path3.txt", ""); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider.Select((t, _) => t.Path).WithTrackingName("Paths"), (spc, p) => { }); })); // run the generator once and check we saw the additional file GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { additionalText1, additionalText2, additionalText3 }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Paths"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path1.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.New, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path1.txt", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path2.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.New, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path2.txt", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path3.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.New, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path3.txt", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // re-run and check nothing else got added driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Paths"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path1.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path1.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path2.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path2.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path3.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path3.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // now, update the additional text with a new path driver = driver.ReplaceAdditionalText(additionalText2, new InMemoryAdditionalText("path4.txt", "")); // run, and check that only the replaced file was invoked driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Paths"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path1.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path1.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path4.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Modified, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path4.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }, step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path3.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path3.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // replace it with null, and check that it throws Assert.Throws<ArgumentNullException>(() => driver.ReplaceAdditionalText(additionalText1, null!)); } [Fact] [WorkItem("https://github.com/dotnet/vscode-csharp/issues/9211")] public void ReplaceAdditionalTexts_Reordered_Does_Not_Duplicate_Entries() { // When the input count is unchanged, InputNode uses a "modify" heuristic instead of // remove+add, which keeps the table the same size and avoids unnecessary downstream // invalidation. This test verifies that reordering surviving items while simultaneously // replacing one does not produce duplicate table entries -- each item appears exactly once, // and only the slot whose item was removed gets a new value. var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var textA = new InMemoryAdditionalText("pathA.txt", "a"); var textB = new InMemoryAdditionalText("pathB.txt", "b"); var textC = new InMemoryAdditionalText("pathC.txt", "c"); var textD = new InMemoryAdditionalText("pathD.txt", "d"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterImplementationSourceOutput( ctx.AdditionalTextsProvider.Select((t, _) => t.Path).WithTrackingName("Paths"), (spc, path) => { spc.AddSource(path.Replace(".", "_"), "// " + path); }); })); // Run 1: [A, B, C] GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { textA, textB, textC }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); // Run 2: [C, A, D] -- reorder C and A, remove B, add D. // The new item D should land in B's old slot. A and C should remain cached. driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textC, textA, textD)); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); Assert.Collection(runResult.Results[0].TrackedSteps["Paths"], step => { Assert.Equal("pathA.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }, step => { Assert.Equal("pathD.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Modified, step.Outputs[0].Reason); }, step => { Assert.Equal("pathC.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); // Run 3: same inputs again -- everything should be cached driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); Assert.Collection(runResult.Results[0].TrackedSteps["Paths"], step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason), step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason), step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason)); } [Fact] [WorkItem("https://github.com/dotnet/vscode-csharp/issues/9211")] public void ReplaceAdditionalTexts_NewItem_Only_Modifies_Replaced_Slot() { // When an item is removed and a new one is added (same count), only the removed item's // slot should receive Modified status. Surviving items must remain Cached regardless of // where they or the new item appear in the input ordering, because the replacement cursor // skips items that exist in the previous table and only selects genuinely new items. var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var textA = new InMemoryAdditionalText("pathA.txt", "a"); var textB = new InMemoryAdditionalText("pathB.txt", "b"); var textC = new InMemoryAdditionalText("pathC.txt", "c"); var textD = new InMemoryAdditionalText("pathD.txt", "d"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterImplementationSourceOutput( ctx.AdditionalTextsProvider.Select((t, _) => t.Path).WithTrackingName("Paths"), (spc, path) => { spc.AddSource(path.Replace(".", "_"), "// " + path); }); })); // Run 1: [A, B, C] GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { textA, textB, textC }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); Assert.Empty(driver.GetRunResult().Diagnostics); // Run 2: [D, B, C] -- remove A (slot 0), add D at the front. // D should replace A's slot. B and C remain cached. Only 1 Modified entry. driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textD, textB, textC)); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); Assert.Collection(runResult.Results[0].TrackedSteps["Paths"], step => { Assert.Equal("pathD.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Modified, step.Outputs[0].Reason); }, step => { Assert.Equal("pathB.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }, step => { Assert.Equal("pathC.txt", step.Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); }); // Run 3: [B, D, C] -- same items, just reorder D and B. // No items changed, so everything should be cached. driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textB, textD, textC)); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Collection(runResult.Results[0].TrackedSteps["Paths"], step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason), step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason), step => Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason)); } [Fact] [WorkItem("https://github.com/dotnet/vscode-csharp/issues/9211")] public void ReplaceAdditionalTexts_Multiple_Replacements_Only_Modify_Replaced_Slots() { // When multiple items are removed and an equal number of new items are added (same total // count), the new items should be assigned to the removed slots in input order. Surviving // items keep their existing table positions with Cached status, ensuring downstream nodes // only re-evaluate the slots that actually changed. var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var textA = new InMemoryAdditionalText("pathA.txt", "a"); var textB = new InMemoryAdditionalText("pathB.txt", "b"); var textC = new InMemoryAdditionalText("pathC.txt", "c"); var textD = new InMemoryAdditionalText("pathD.txt", "d"); var textE = new InMemoryAdditionalText("pathE.txt", "e"); var textX = new InMemoryAdditionalText("pathX.txt", "x"); var textY = new InMemoryAdditionalText("pathY.txt", "y"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterImplementationSourceOutput( ctx.AdditionalTextsProvider.Select((t, _) => t.Path).WithTrackingName("Paths"), (spc, path) => { spc.AddSource(path.Replace(".", "_"), "// " + path); }); })); // Run 1: [A, B, C, D, E] GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { textA, textB, textC, textD, textE }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); Assert.Empty(driver.GetRunResult().Diagnostics); // Run 2: [X, C, Y, D, A] -- remove B (slot 1) and E (slot 4), add X and Y. // A, C, D survive. X and Y should fill the two removed slots. driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textX, textC, textY, textD, textA)); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(5, runResult.GeneratedTrees.Length); var steps = runResult.Results[0].TrackedSteps["Paths"]; Assert.Equal(5, steps.Length); // A (slot 0): survived -> Cached Assert.Equal("pathA.txt", steps[0].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, steps[0].Outputs[0].Reason); // B's old slot (1): replaced by X (first new item in input order) -> Modified Assert.Equal("pathX.txt", steps[1].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Modified, steps[1].Outputs[0].Reason); // C (slot 2): survived -> Cached Assert.Equal("pathC.txt", steps[2].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, steps[2].Outputs[0].Reason); // D (slot 3): survived -> Cached Assert.Equal("pathD.txt", steps[3].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, steps[3].Outputs[0].Reason); // E's old slot (4): replaced by Y (second new item in input order) -> Modified Assert.Equal("pathY.txt", steps[4].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Modified, steps[4].Outputs[0].Reason); } [Fact] [WorkItem("https://github.com/dotnet/vscode-csharp/issues/9211")] public void ReplaceAdditionalTexts_Successive_Shuffles_Maintain_Correct_State() { // The input node table must remain consistent across multiple rounds of shuffling and // replacing items. Items that survive every round should stay Cached, new items should // be Modified in the round they appear, and a subsequent run with identical inputs should // produce all Cached entries -- confirming the table converges to a stable state. var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var textA = new InMemoryAdditionalText("pathA.txt", "a"); var textB = new InMemoryAdditionalText("pathB.txt", "b"); var textC = new InMemoryAdditionalText("pathC.txt", "c"); var textD = new InMemoryAdditionalText("pathD.txt", "d"); var textE = new InMemoryAdditionalText("pathE.txt", "e"); var textF = new InMemoryAdditionalText("pathF.txt", "f"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterImplementationSourceOutput( ctx.AdditionalTextsProvider.Select((t, _) => t.Path).WithTrackingName("Paths"), (spc, path) => { spc.AddSource(path.Replace(".", "_"), "// " + path); }); })); // Run 1: [A, B, C] GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { textA, textB, textC }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); Assert.Empty(driver.GetRunResult().Diagnostics); // Run 2: [C, D, A] -- remove B, add D, shuffle driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textC, textD, textA)); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); // Run 3: [E, A, F] -- remove C and D, add E and F, shuffle driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textE, textA, textF)); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); Assert.Equal(3, runResult.GeneratedTrees.Length); // A should be Cached (survived all rounds). E and F should be Modified (replaced removed slots). var steps = runResult.Results[0].TrackedSteps["Paths"]; Assert.Equal(3, steps.Length); Assert.Equal("pathA.txt", steps[0].Outputs[0].Value); Assert.Equal(IncrementalStepRunReason.Cached, steps[0].Outputs[0].Reason); Assert.Equal(IncrementalStepRunReason.Modified, steps[1].Outputs[0].Reason); Assert.Equal(IncrementalStepRunReason.Modified, steps[2].Outputs[0].Reason); // The two modified slots should contain E and F (in input order of new items) var modifiedValues = steps.Where(s => s.Outputs[0].Reason == IncrementalStepRunReason.Modified) .Select(s => (string)s.Outputs[0].Value) .ToArray(); Assert.Equal(new[] { "pathE.txt", "pathF.txt" }, modifiedValues); // Run 4: same inputs [E, A, F] again -- everything should be cached driver = driver.ReplaceAdditionalTexts(ImmutableArray.Create<AdditionalText>(textE, textA, textF)); driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult(); Assert.Empty(runResult.Diagnostics); foreach (var step in runResult.Results[0].TrackedSteps["Paths"]) { Assert.Equal(IncrementalStepRunReason.Cached, step.Outputs[0].Reason); } } [Fact] public void Replaced_Input_Is_Treated_As_Modified() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); InMemoryAdditionalText additionalText = new InMemoryAdditionalText("path.txt", "abc"); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var texts = ctx.AdditionalTextsProvider; var paths = texts.Select((t, _) => t?.Path).WithTrackingName("Path"); var contents = texts.Select((t, ct) => t?.GetText(ct)?.ToString()).WithTrackingName("Content"); ctx.RegisterSourceOutput(paths, (spc, p) => { }); ctx.RegisterSourceOutput(contents, (spc, p) => { }); })); // run the generator once and check we saw the additional file GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, additionalTexts: new[] { additionalText }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Path"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.New, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path.txt", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["Content"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.New, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("abc", output.Value); Assert.Equal(IncrementalStepRunReason.New, output.Reason); }); }); // re-run and check nothing else got added driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Path"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["Content"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Cached, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("abc", output.Value); Assert.Equal(IncrementalStepRunReason.Cached, output.Reason); }); }); // now, update the additional text, but keep the path the same var secondText = new InMemoryAdditionalText("path.txt", "def"); driver = driver.ReplaceAdditionalText(additionalText, secondText); // run, and check that only the contents are marked as modified driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Path"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Modified, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Unchanged, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["Content"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Modified, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("def", output.Value); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }); // now replace the text with a different path, but the same text var thirdText = new InMemoryAdditionalText("path2.txt", "def"); driver = driver.ReplaceAdditionalText(secondText, thirdText); // run, and check that only the paths got re-run driver = driver.RunGenerators(compilation); runResult = driver.GetRunResult().Results[0]; Assert.Collection(runResult.TrackedSteps["Path"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path2.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Modified, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("path2.txt", output.Value); Assert.Equal(IncrementalStepRunReason.Modified, output.Reason); }); }); Assert.Collection(runResult.TrackedSteps["Content"], step => { Assert.Collection(step.Inputs, input => { var consumedInput = input.Source.Outputs[input.OutputIndex]; Assert.Equal("path2.txt", Assert.IsType<InMemoryAdditionalText>(consumedInput.Value).Path); Assert.Equal(IncrementalStepRunReason.Modified, consumedInput.Reason); }); Assert.Collection(step.Outputs, output => { Assert.Equal("def", output.Value); Assert.Equal(IncrementalStepRunReason.Unchanged, output.Reason); }); }); } #pragma warning disable RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable RSEXPERIMENTAL007 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. [Theory] [CombinatorialData] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.Implementation)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.PostInit)] [InlineData(IncrementalGeneratorOutputKind.Implementation | IncrementalGeneratorOutputKind.PostInit)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.Host)] [InlineData(IncrementalGeneratorOutputKind.Implementation | IncrementalGeneratorOutputKind.Host)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.Implementation | IncrementalGeneratorOutputKind.PostInit)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.Implementation | IncrementalGeneratorOutputKind.PostInit | IncrementalGeneratorOutputKind.Host)] [InlineData(IncrementalGeneratorOutputKind.PreCompilation)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.PreCompilation)] [InlineData(IncrementalGeneratorOutputKind.Source | IncrementalGeneratorOutputKind.Implementation | IncrementalGeneratorOutputKind.PostInit | IncrementalGeneratorOutputKind.Host | IncrementalGeneratorOutputKind.PreCompilation)] public void Generator_Output_Kinds_Can_Be_Disabled(IncrementalGeneratorOutputKind disabledOutput) { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterPostInitializationOutput((context) => context.AddSource("PostInit", "")); ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => context.AddSource("Source", "")); ctx.RegisterImplementationSourceOutput(ctx.CompilationProvider, (context, ct) => context.AddSource("Implementation", "")); ctx.RegisterHostOutput(ctx.CompilationOptionsProvider, (context, ct) => context.AddOutput("Host", "")); ctx.RegisterPreCompilationSourceOutput(ctx.ParseOptionsProvider, (context, ct) => context.AddSource("PreCompilation", "")); }); GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: [generator.AsSourceGenerator()], driverOptions: new GeneratorDriverOptions(disabledOutput), parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var result = driver.GetRunResult(); Assert.Single(result.Results); Assert.Empty(result.Results[0].Diagnostics); // verify the expected outputs were generated // NOTE: adding new output types will cause this test to fail. Update above as needed. foreach (IncrementalGeneratorOutputKind kind in Enum.GetValues(typeof(IncrementalGeneratorOutputKind))) { if (kind == IncrementalGeneratorOutputKind.None) continue; if (disabledOutput.HasFlag(kind)) { if (kind == IncrementalGeneratorOutputKind.Host) { Assert.DoesNotContain(result.Results[0].HostOutputs, o => o.Key == "Host"); } else { Assert.DoesNotContain(result.Results[0].GeneratedSources, isTextForKind); } } else { if (kind == IncrementalGeneratorOutputKind.Host) { Assert.Contains(result.Results[0].HostOutputs, o => o.Key == "Host"); } else { Assert.Contains(result.Results[0].GeneratedSources, isTextForKind); } } bool isTextForKind(GeneratedSourceResult s) => s.HintName == Enum.GetName(typeof(IncrementalGeneratorOutputKind), kind) + ".cs"; } } #pragma warning restore RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning restore RSEXPERIMENTAL007 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. [Fact] public void IncrementalGeneratorInputSourcesHaveNames() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.SyntaxProvider.CreateSyntaxProvider((node, ct) => node is ClassDeclarationSyntax c, (context, ct) => context.Node).WithTrackingName("Syntax"), (context, ct) => { }); ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.AnalyzerConfigOptionsProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider, (context, ct) => { }); ctx.RegisterImplementationSourceOutput(ctx.MetadataReferencesProvider, (context, ct) => { }); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, additionalTexts: new[] { new InMemoryAdditionalText("text.txt", "") }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult().Results[0]; // Assert that the well-named providers recorded steps with well known names. Assert.Contains(WellKnownGeneratorInputs.Compilation, runResult.TrackedSteps.Keys); Assert.Contains(WellKnownGeneratorInputs.AnalyzerConfigOptions, runResult.TrackedSteps.Keys); Assert.Contains(WellKnownGeneratorInputs.ParseOptions, runResult.TrackedSteps.Keys); Assert.Contains(WellKnownGeneratorInputs.AdditionalTexts, runResult.TrackedSteps.Keys); Assert.Contains(WellKnownGeneratorInputs.MetadataReferences, runResult.TrackedSteps.Keys); // Assert that a syntax provider records itself. Assert.Contains("Syntax", runResult.TrackedSteps.Keys); // Source output steps have the well-defined SourceOutputStep name Assert.Contains(WellKnownGeneratorOutputs.SourceOutput, runResult.TrackedSteps.Keys); Assert.Contains(WellKnownGeneratorOutputs.ImplementationSourceOutput, runResult.TrackedSteps.Keys); // Source output steps should also be in the TrackedOutputSteps collection Assert.Contains(WellKnownGeneratorOutputs.SourceOutput, runResult.TrackedOutputSteps.Keys); Assert.Contains(WellKnownGeneratorOutputs.ImplementationSourceOutput, runResult.TrackedOutputSteps.Keys); Assert.Equal(8, runResult.TrackedSteps.Count); Assert.Equal(2, runResult.TrackedOutputSteps.Count); } [Fact] public void Steps_From_Common_Input_Nodes_Recorded_In_All_Generators_Steps() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); InMemoryAdditionalText additionalText = new InMemoryAdditionalText("path.txt", "abc"); var generator1 = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider, (context, ct) => { }); }); var generator2 = new PipelineCallbackGenerator2(ctx => { ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider, (context, ct) => { }); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator1.AsSourceGenerator(), generator2.AsSourceGenerator() }, parseOptions: parseOptions, additionalTexts: new[] { additionalText }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); GeneratorDriverRunResult runResult = driver.GetRunResult(); Assert.All(runResult.Results, result => Assert.Contains(WellKnownGeneratorInputs.AdditionalTexts, result.TrackedSteps.Keys)); Assert.Equal(2, runResult.Results.Length); } [Fact] public void Metadata_References_Provider() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; PortableExecutableReference[] metadataRefs = [ MetadataReference.CreateFromAssemblyInternal(this.GetType().Assembly), MetadataReference.CreateFromAssemblyInternal(typeof(object).Assembly) ]; Compilation compilation = CreateEmptyCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions, references: metadataRefs); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); List<string?> referenceList = new List<string?>(); var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.MetadataReferencesProvider, (spc, r) => { referenceList.Add(r.Display); }); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); Assert.Equal(referenceList[0], metadataRefs[0].Display); Assert.Equal(referenceList[1], metadataRefs[1].Display); // re-run and check we didn't see anything new referenceList.Clear(); driver = driver.RunGenerators(compilation); Assert.Empty(referenceList); // Modify the reference var modifiedRef = metadataRefs[0].WithAliases(new[] { "Alias " }); metadataRefs[0] = modifiedRef; compilation = compilation.WithReferences(metadataRefs); driver = driver.RunGenerators(compilation); Assert.Single(referenceList, modifiedRef.Display); } [ConditionalFact(typeof(NoIOperationValidation))] [WorkItem(59190, "https://github.com/dotnet/roslyn/issues/59190")] public void LongBinaryExpression() { const int count = 7000; const string header = """ class C { public static readonly string F = "a" """; const string expr = @" + ""a"""; var capacity = header.Length + (expr.Length * count) + 5; var builder = new StringBuilder(capacity); builder.Append(header); for (int i = 0; i < count; i++) { builder.Append(expr); } builder.AppendLine(";"); builder.Append("}"); Assert.True(capacity >= builder.Capacity); var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(builder.ToString(), options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.SyntaxProvider.CreateSyntaxProvider((node, ct) => node is ClassDeclarationSyntax c, (context, ct) => context.Node).WithTrackingName("Syntax"), (context, ct) => { }); ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.AnalyzerConfigOptionsProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (context, ct) => { }); ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider, (context, ct) => { }); ctx.RegisterImplementationSourceOutput(ctx.MetadataReferencesProvider, (context, ct) => { }); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, additionalTexts: new[] { new InMemoryAdditionalText("text.txt", "") }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); driver.GetRunResult(); } [Fact] [WorkItem(59209, "https://github.com/dotnet/roslyn/issues/59209")] public void Binary_Additional_Files_Do_Not_Throw_When_Compared() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.AdditionalTextsProvider, (context, text) => { context.AddSource(Path.GetFileName(text.Path), ""); }); }); var additionalText1 = new InMemoryAdditionalText.BinaryText("file1"); var additionalText2 = new InMemoryAdditionalText.BinaryText("file2"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, additionalTexts: new[] { additionalText1, additionalText2 }, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); driver = driver.RunGenerators(compilation); } [Fact] [WorkItem(58625, "https://github.com/dotnet/roslyn/issues/58625")] public void Incremental_Generators_Can_Recover_From_Exceptions() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); bool shouldThrow = true; var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { if (shouldThrow) { throw new InvalidOperationException(); } else { context.AddSource("generated", ""); } }); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var result = driver.GetRunResult(); var diag = Assert.Single(result.Diagnostics); // update the compilation compilation = compilation.WithOptions(compilation.Options.WithModuleName("newName")); shouldThrow = false; driver = driver.RunGenerators(compilation); result = driver.GetRunResult(); Assert.Single(result.GeneratedTrees); } [Fact] public void Timing_Info_Is_Empty_If_Not_Run() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { context.AddSource("generated", ""); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); var timing = driver.GetTimingInfo(); Assert.Equal(TimeSpan.Zero, timing.ElapsedTime); var generatorTiming = Assert.Single(timing.GeneratorTimes); Assert.Equal(generator, generatorTiming.Generator); Assert.Equal(TimeSpan.Zero, generatorTiming.ElapsedTime); } [Fact] public void Can_Get_Timing_Info() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { context.AddSource("generated", ""); Thread.Sleep(1); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var timing = driver.GetTimingInfo(); Assert.NotEqual(TimeSpan.Zero, timing.ElapsedTime); var generatorTiming = Assert.Single(timing.GeneratorTimes); Assert.Equal(generator, generatorTiming.Generator); Assert.NotEqual(TimeSpan.Zero, generatorTiming.ElapsedTime); Assert.True(timing.ElapsedTime >= generatorTiming.ElapsedTime); } [Fact] public void Can_Get_Timing_Info_From_Multiple_Generators() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { context.AddSource("generated", ""); Thread.Sleep(1); }); }).AsSourceGenerator(); var generator2 = new PipelineCallbackGenerator2(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { context.AddSource("generated", ""); Thread.Sleep(1); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator, generator2 }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var timing = driver.GetTimingInfo(); Assert.NotEqual(TimeSpan.Zero, timing.ElapsedTime); Assert.Equal(2, timing.GeneratorTimes.Length); var timing1 = timing.GeneratorTimes[0]; Assert.Equal(generator, timing1.Generator); Assert.NotEqual(TimeSpan.Zero, timing1.ElapsedTime); Assert.True(timing.ElapsedTime >= timing1.ElapsedTime); var timing2 = timing.GeneratorTimes[1]; Assert.Equal(generator2, timing2.Generator); Assert.NotEqual(TimeSpan.Zero, timing2.ElapsedTime); Assert.True(timing.ElapsedTime >= timing2.ElapsedTime); Assert.True(timing.ElapsedTime >= timing1.ElapsedTime + timing2.ElapsedTime); } [Fact] public void Timing_Info_Only_Includes_Last_Run() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { Thread.Sleep(50); context.AddSource("generated", ""); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); // run once driver = driver.RunGenerators(compilation); var timing = driver.GetTimingInfo(); Assert.NotEqual(TimeSpan.Zero, timing.ElapsedTime); var generatorTiming = Assert.Single(timing.GeneratorTimes); Assert.Equal(generator, generatorTiming.Generator); Assert.NotEqual(TimeSpan.Zero, generatorTiming.ElapsedTime); Assert.True(timing.ElapsedTime >= generatorTiming.ElapsedTime); // run a second time. No steps should be performed, so overall time should be less driver = driver.RunGenerators(compilation); var timing2 = driver.GetTimingInfo(); Assert.NotEqual(TimeSpan.Zero, timing2.ElapsedTime); Assert.True(timing.ElapsedTime > timing2.ElapsedTime); var generatorTiming2 = Assert.Single(timing2.GeneratorTimes); Assert.Equal(generator, generatorTiming2.Generator); Assert.NotEqual(TimeSpan.Zero, generatorTiming2.ElapsedTime); Assert.True(generatorTiming.ElapsedTime > generatorTiming2.ElapsedTime); } [Fact] public void Returning_Null_From_SelectMany_Gives_Empty_Array() { var source = "class C{}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { var nullArray = ctx.CompilationProvider.Select((c, _) => null as object[]); var flatArray = nullArray.SelectMany((a, _) => a!); ctx.RegisterSourceOutput(flatArray, (_, _) => { }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Empty(runResult.GeneratedTrees); Assert.Empty(runResult.Diagnostics); var result = Assert.Single(runResult.Results); Assert.Empty(result.GeneratedSources); Assert.Empty(result.Diagnostics); } [Fact] public void Post_Init_Trees_Are_Reparsed_When_ParseOptions_Change() { var source = "class C{}"; var postInitSource = @" #pragma warning disable CS0169 class D { (int, bool) _field; }"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterPostInitializationOutput(c => c.AddSource("D", postInitSource)); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); compilation.VerifyDiagnostics(); Assert.Empty(diagnostics); // change the parse options so that the tree is no longer accepted parseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp2); compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); driver = driver.WithUpdatedParseOptions(parseOptions); // change some other options to ensure the parseOption change tracking flows correctly driver = driver.AddAdditionalTexts(ImmutableArray<AdditionalText>.Empty); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out diagnostics); diagnostics.Verify(); compilation.VerifyDiagnostics( // Microsoft.CodeAnalysis.Test.Utilities\Roslyn.Test.Utilities.TestGenerators.PipelineCallbackGenerator\D.cs(3,12): error CS8022: Feature 'tuples' is not available in C# 2. Please use language version 7.0 or greater. // class D { (int, bool) _field; } Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion2, "(int, bool)").WithArguments("tuples", "7.0").WithLocation(3, 12) ); // change them back to something where it is supported parseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp8); compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); driver = driver.WithUpdatedParseOptions(parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out diagnostics); diagnostics.Verify(); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_DetachedSyntaxTree_Incremental() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (ctx, _) => { var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "/detached"); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)))); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), "Reported diagnostic 'TEST0001' has a source location in file '/detached', which is not part of the compilation being analyzed.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_DetachedSyntaxTree_Incremental_AdditionalLocations() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (ctx, comp) => { var validSyntaxTree = comp.SyntaxTrees.Single(); var invalidSyntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "/detached"); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(validSyntaxTree, TextSpan.FromBounds(0, 2)), additionalLocations: new[] { Location.Create(invalidSyntaxTree, TextSpan.FromBounds(0, 2)) })); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), "Reported diagnostic 'TEST0001' has a source location in file '/detached', which is not part of the compilation being analyzed.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_DetachedSyntaxTree_Execute() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new CallbackGenerator(ctx => { }, ctx => { var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "/detached"); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)))); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(CallbackGenerator), "Reported diagnostic 'TEST0001' has a source location in file '/detached', which is not part of the compilation being analyzed.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_DetachedSyntaxTree_Execute_AdditionalLocations() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new CallbackGenerator(ctx => { }, ctx => { var validSyntaxTree = ctx.Compilation.SyntaxTrees.Single(); var invalidSyntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "/detached"); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(validSyntaxTree, TextSpan.FromBounds(0, 2)), additionalLocations: new[] { Location.Create(invalidSyntaxTree, TextSpan.FromBounds(0, 2)) })); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(CallbackGenerator), "Reported diagnostic 'TEST0001' has a source location in file '/detached', which is not part of the compilation being analyzed.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpanOutsideRange_Incremental() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (ctx, comp) => { var syntaxTree = comp.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 100)))); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), "Reported diagnostic 'TEST0001' has a source location '[0..100)' in file '/original', which is outside of the given file.", "diagnostic"); compilation.VerifyDiagnostics(); } [Fact] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] [WorkItem("https://github.com/dotnet/roslyn/issues/82032")] public void Diagnostic_SpanOutsideRange_Incremental_Update() { var source = "class SomewhatLongClassName {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { var inputs = ctx.SyntaxProvider.CreateSyntaxProvider( static (node, ct) => node.IsKind(SyntaxKind.ClassDeclaration), static (ctx, ct) => 0); ctx.RegisterSourceOutput(inputs, (ctx, input) => { var tree = compilation.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(tree, TextSpan.FromBounds(20, 21)))); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); diagnostics.Verify(Diagnostic("TEST0001", "a").WithLocation(1, 21)); source = "class C {}"; compilation = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.Single(), CSharpSyntaxTree.ParseText(source, parseOptions, path: "/original")); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), string.Format(CodeAnalysisResources.InvalidDiagnosticLocationReported, "TEST0001", "/original"), "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpanOutsideRange_Incremental_AdditionalLocations() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (ctx, comp) => { var syntaxTree = comp.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)), additionalLocations: new[] { Location.Create(syntaxTree, TextSpan.FromBounds(0, 100)) })); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), "Reported diagnostic 'TEST0001' has a source location '[0..100)' in file '/original', which is outside of the given file.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpanOutsideRange_Execute() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); compilation.VerifyDiagnostics(); var generator = new CallbackGenerator(ctx => { }, ctx => { var syntaxTree = ctx.Compilation.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 100)))); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(CallbackGenerator), "Reported diagnostic 'TEST0001' has a source location '[0..100)' in file '/original', which is outside of the given file.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpanOutsideRange_Execute_AdditionalLocations() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); compilation.VerifyDiagnostics(); var generator = new CallbackGenerator(ctx => { }, ctx => { var syntaxTree = ctx.Compilation.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)), additionalLocations: new[] { Location.Create(syntaxTree, TextSpan.FromBounds(0, 100)) })); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(CallbackGenerator), "Reported diagnostic 'TEST0001' has a source location '[0..100)' in file '/original', which is outside of the given file.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpaceInIdentifier_Incremental() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new PipelineCallbackGenerator(ctx => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (ctx, comp) => { var syntaxTree = comp.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST 0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)))); }); }).AsSourceGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), "Reported diagnostic has an ID 'TEST 0001', which is not a valid identifier.", "diagnostic"); compilation.VerifyDiagnostics(); } [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpaceInIdentifier_Execute() { var source = "class C {}"; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); compilation.VerifyDiagnostics(); var generator = new CallbackGenerator(ctx => { }, ctx => { var syntaxTree = ctx.Compilation.SyntaxTrees.Single(); ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( "TEST 0001", "Test", "Test diagnostic", DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, isEnabledByDefault: true, warningLevel: 1, location: Location.Create(syntaxTree, TextSpan.FromBounds(0, 2)))); }); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(CallbackGenerator), "Reported diagnostic has an ID 'TEST 0001', which is not a valid identifier.", "diagnostic"); compilation.VerifyDiagnostics(); } [Fact] public void IncrementalGenerator_Add_New_Generator_After_Generation() { // 1. run a generator, smuggling out some inputs from context // 2. add a second generator, re-using the inputs from the first step and using a Combine node // 3. run the new graph var source = @" class C { } "; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing); compilation.VerifyDiagnostics(); IncrementalValueProvider<ParseOptions> parseOptionsProvider = default; IncrementalValueProvider<AnalyzerConfigOptionsProvider> configOptionsProvider = default; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var source = parseOptionsProvider = ctx.ParseOptionsProvider; var source2 = configOptionsProvider = ctx.AnalyzerConfigOptionsProvider; var combine = source.Combine(source2); ctx.RegisterSourceOutput(combine, (spc, c) => { }); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }); driver = driver.RunGenerators(compilation); // parse options and analyzer options are now cached // add a new generator that depends on them bool wasCalled = false; var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(ctx => { var source = parseOptionsProvider; var source2 = configOptionsProvider; // this call should always be made, even though the above inputs are cached var transform = source.Select((a, _) => { wasCalled = true; return new object(); }); // now combine source2 with the transform. Combine will call single on transform, and we'll crash if it wasn't called var combine = source2.Combine(transform); ctx.RegisterSourceOutput(combine, (spc, c) => { }); })); driver = driver.AddGenerators(ImmutableArray.Create<ISourceGenerator>(generator2)); driver = driver.RunGenerators(compilation); Assert.True(wasCalled); } [Fact] public void IncrementalGenerator_Add_New_Generator_After_Generation_SourceOutputNode() { // 1. run a generator, smuggling out some inputs from context // 2. add a second generator, re-using the inputs from the first step // 3. run the new graph var source = @" class C { } "; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing); compilation.VerifyDiagnostics(); IncrementalValueProvider<ParseOptions> parseOptionsProvider = default; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var source = parseOptionsProvider = ctx.ParseOptionsProvider; ctx.RegisterSourceOutput(source, (spc, c) => { }); })); GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }); driver = driver.RunGenerators(compilation); // parse options are now cached // add a new generator that depends on them bool wasCalled = false; var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(ctx => { var source = parseOptionsProvider; ctx.RegisterSourceOutput(source, (spc, c) => { wasCalled = true; }); })); driver = driver.AddGenerators(ImmutableArray.Create<ISourceGenerator>(generator2)); driver = driver.RunGenerators(compilation); Assert.True(wasCalled); } [Fact] public void IncrementalGenerator_Add_New_Generator_With_Syntax_After_Generation() { var source = @" class C { } "; var parseOptions = TestOptions.RegularPreview; Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); compilation.VerifyDiagnostics(); Assert.Single(compilation.SyntaxTrees); bool gen1Called = false; var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var syntax = ctx.SyntaxProvider.CreateSyntaxProvider((s, _) => true, (s, _) => s.Node); ctx.RegisterSourceOutput(syntax, (spc, c) => { gen1Called = true; }); })); // run the generator and make sure the first node is cached GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions); driver = driver.RunGenerators(compilation); Assert.True(gen1Called); // now, add another syntax node from another generator var gen2Called = false; var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(ctx => { var syntax = ctx.SyntaxProvider.CreateSyntaxProvider((s, _) => true, (s, _) => s.Node); ctx.RegisterSourceOutput(syntax, (spc, c) => { gen2Called = true; }); })); driver = driver.AddGenerators(ImmutableArray.Create<ISourceGenerator>(generator2)); // ensure it runs successfully gen1Called = false; driver = driver.RunGenerators(compilation); Assert.False(gen1Called); // Generator 1 did not re-run Assert.True(gen2Called); } [Fact, WorkItem(66451, "https://github.com/dotnet/roslyn/issues/66451")] public void Transform_MultipleInputs_RemoveFirst_ModifySecond() { var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { var provider = ctx.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is ClassDeclarationSyntax c, static (gsc, _) => gsc.Node) .Select(static (node, _) => (ClassDeclarationSyntax)node) .Where(static (node) => node.Modifiers.Any(SyntaxKind.PartialKeyword)) .WithTrackingName("MyTransformNode"); ctx.RegisterSourceOutput(provider, static (spc, syntax) => { spc.AddSource( $"{syntax.Identifier.Text}.g", $"partial class {syntax.Identifier.Text} {{ /* generated */ }}"); }); })); var parseOptions = TestOptions.RegularPreview; var source1 = """ public partial class Class1 { } """; var source2 = """ public partial class Class2 { } """; Compilation compilation = CreateCompilation(new[] { source1, source2 }, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); verify(ref driver, compilation); // Remove Class1 from the final provider via a TransformNode // (by removing the partial keyword). replace(ref compilation, parseOptions, "Class1", """ public class Class1 { } """); verify(ref driver, compilation); // Modify Class2 (make it internal). replace(ref compilation, parseOptions, "Class2", """ internal partial class Class2 { } """); verify(ref driver, compilation); static void verify(ref GeneratorDriver driver, Compilation compilation) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); generatorDiagnostics.Verify(); } static void replace(ref Compilation compilation, CSharpParseOptions parseOptions, string className, string source) { var tree = compilation.GetMember(className).DeclaringSyntaxReferences.Single().SyntaxTree; compilation = compilation.ReplaceSyntaxTree(tree, CSharpSyntaxTree.ParseText(source, parseOptions)); } } private void VerifyArgumentExceptionDiagnostic( Diagnostic diagnostic, string generatorName, string message, string parameterName, bool initialization = false) { _output.WriteLine(diagnostic.ToString()); var expectedMessage = new ArgumentException(message: message, paramName: parameterName).Message; VerifyGeneratorExceptionDiagnostic<ArgumentException>(diagnostic, generatorName, expectedMessage, initialization); } internal static void VerifyGeneratorExceptionDiagnostic<T>( Diagnostic diagnostic, string generatorName, string message, bool initialization = false) where T : Exception { var errorCode = initialization ? ErrorCode.WRN_GeneratorFailedDuringInitialization : ErrorCode.WRN_GeneratorFailedDuringGeneration; Assert.Equal("CS" + (int)errorCode, diagnostic.Id); Assert.Equal(NoLocation.Singleton, diagnostic.Location); Assert.Equal(4, diagnostic.Arguments.Count); Assert.Equal(generatorName, diagnostic.Arguments[0]); var typeName = typeof(T).Name; Assert.Equal(typeName, diagnostic.Arguments[1]); Assert.Equal(message, diagnostic.Arguments[2]); var expectedDetails = $"System.{typeName}: {message}{Environment.NewLine} "; Assert.StartsWith(expectedDetails, diagnostic.Arguments[3] as string); } [Fact] public void GetInterceptsLocationSpecifier_01() { var generator = new IncrementalGeneratorWrapper(new InterceptorGenerator1()); var parseOptions = TestOptions.RegularPreview.WithFeature(Feature.InterceptorsNamespaces, "global"); var projectDir = TempRoot.Root; var source1 = (""" public class Program { public static void Main() { var program = new Program(); program.M(1); } public void M(int param) => throw null!; } namespace System.Runtime.CompilerServices { public class InterceptsLocationAttribute : Attribute { public InterceptsLocationAttribute(int version, string data) { } } } """, Path.Combine(projectDir, "src", "Program.cs")); Compilation compilation = CreateCompilation([source1], options: TestOptions.DebugExe, parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create( [generator], parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(baseDirectory: Path.Combine(projectDir, "obj"))); verify(ref driver, compilation); void verify(ref GeneratorDriver driver, Compilation compilation) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); outputCompilation.VerifyDiagnostics(); CompileAndVerify(outputCompilation, expectedOutput: "1"); generatorDiagnostics.Verify(); } } [Generator(LanguageNames.CSharp)] private class InterceptorGenerator1 : IIncrementalGenerator { #pragma warning disable RSEXPERIMENTAL002 // test record InterceptorInfo(InterceptableLocation locationSpecifier, object data); private static bool IsInterceptableCall(SyntaxNode node, CancellationToken token) => node is InvocationExpressionSyntax; private static object GetData(GeneratorSyntaxContext context) => 1; public void Initialize(IncrementalGeneratorInitializationContext context) { var interceptorInfos = context.SyntaxProvider.CreateSyntaxProvider( predicate: IsInterceptableCall, transform: (GeneratorSyntaxContext context, CancellationToken token) => { var model = context.SemanticModel; var locationSpecifier = model.GetInterceptableLocation((InvocationExpressionSyntax)context.Node, token); if (locationSpecifier is null) { return null; // generator wants to intercept call, but host thinks call is not interceptable. bug. } // generator is careful to propagate only equatable data (i.e., not syntax nodes or symbols). return new InterceptorInfo(locationSpecifier, GetData(context)); }) .Where(info => info != null) .Collect(); context.RegisterSourceOutput(interceptorInfos, (context, interceptorInfos) => { var builder = new StringBuilder(); builder.AppendLine("using System.Runtime.CompilerServices;"); builder.AppendLine("using System;"); builder.AppendLine("public static class Interceptors"); builder.AppendLine("{"); // builder boilerplate.. foreach (var interceptorInfo in interceptorInfos) { var (locationSpecifier, data) = interceptorInfo!; builder.AppendLine($$""" // {{locationSpecifier.GetDisplayLocation()}} [InterceptsLocation({{locationSpecifier.Version}}, "{{locationSpecifier.Data}}")] public static void Interceptor(this Program program, int param) { Console.Write(1); } """); } // builder boilerplate.. builder.AppendLine("}"); context.AddSource("MyInterceptors.cs", builder.ToString()); }); } } [Fact] public void SourceGenerator_As_IncrementalGenerator_As_SourceGenerator() { ISourceGenerator generator = new TestSourceGenerator(); var incrementalGenerator = generator.AsIncrementalGenerator(); var sourceGenerator = incrementalGenerator.AsSourceGenerator(); Assert.Same(generator, sourceGenerator); } [Fact] public void SourceGenerator_As_IncrementalGenerator_GetGeneratorType() { ISourceGenerator generator = new TestSourceGenerator(); var incrementalGenerator = generator.AsIncrementalGenerator(); var type = incrementalGenerator.GetGeneratorType(); Assert.Same(generator.GetType(), type); } [Fact] public void IncrementalGenerator_As_SourceGenerator_As_IncrementalGenerator() { IIncrementalGenerator generator = new PipelineCallbackGenerator(ctx => { }); var sourceGenerator = generator.AsSourceGenerator(); var incrementalGenerator = sourceGenerator.AsIncrementalGenerator(); Assert.Same(generator, incrementalGenerator); } [Fact] public void IncrementalGenerator_As_SourceGenerator_GetGeneratorType() { IIncrementalGenerator generator = new PipelineCallbackGenerator(ctx => { }); var sourceGenerator = generator.AsSourceGenerator(); var type = sourceGenerator.GetGeneratorType(); Assert.Same(generator.GetType(), type); } [Fact] public void GeneratorDriver_CreateWith_Wrapped_ISourceGenerator() { bool executeCalled = false; ISourceGenerator generator = new TestSourceGenerator() { ExecuteImpl = (context) => { executeCalled = true; } }; var incrementalGenerator = generator.AsIncrementalGenerator(); var generatorDriver = CSharpGeneratorDriver.Create(incrementalGenerator); generatorDriver.RunGenerators(CreateCompilation("class C { }")); Assert.True(executeCalled); } [Fact] public void GeneratorDriver_CanFilter_GeneratorsToRun() { var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen1Source.cs", c.SyntaxTrees.First().GetRoot().ToFullString() + " //generator1"); }); }).AsSourceGenerator(); var generator2 = new PipelineCallbackGenerator2((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen2Source.cs", c.SyntaxTrees.First().GetRoot().ToFullString() + " //generator2"); }); }).AsSourceGenerator(); var compilation = CreateCompilation("class Compilation1{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(generator1, generator2); driver = driver.RunGenerators(compilation); var result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); Assert.Equal("class Compilation1{} //generator1", result.GeneratedTrees[0].ToString()); Assert.Equal("class Compilation1{} //generator2", result.GeneratedTrees[1].ToString()); // change the generated source, but only run generator 1 compilation = CreateCompilation("class Compilation2{}"); driver = driver.RunGenerators(compilation, ctx => ctx.Generator == generator1); result = driver.GetRunResult(); // only the first generator should have run, generator 2 hasn't been updated Assert.Equal(2, result.GeneratedTrees.Length); Assert.Equal("class Compilation2{} //generator1", result.GeneratedTrees[0].ToString()); Assert.Equal("class Compilation1{} //generator2", result.GeneratedTrees[1].ToString()); // now only run generator 2 compilation = CreateCompilation("class Compilation3{}"); driver = driver.RunGenerators(compilation, ctx => ctx.Generator == generator2); result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); Assert.Equal("class Compilation2{} //generator1", result.GeneratedTrees[0].ToString()); Assert.Equal("class Compilation3{} //generator2", result.GeneratedTrees[1].ToString()); // run everything compilation = CreateCompilation("class Compilation4{}"); driver = driver.RunGenerators(compilation); result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); Assert.Equal("class Compilation4{} //generator1", result.GeneratedTrees[0].ToString()); Assert.Equal("class Compilation4{} //generator2", result.GeneratedTrees[1].ToString()); } [Fact] public void GeneratorDriver_CanFilter_GeneratorsToRun_AndUpdateCompilation() { var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen1Source.cs", "//" + c.SyntaxTrees.First().GetRoot().ToFullString() + " generator1"); }); }).AsSourceGenerator(); var generator2 = new PipelineCallbackGenerator2((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen2Source.cs", "//" + c.SyntaxTrees.First().GetRoot().ToFullString() + " generator2"); }); }).AsSourceGenerator(); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1, generator2], parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var outputDiagnostics); Assert.Empty(outputDiagnostics); // change the generated source, but only run generator 1 compilation = CreateCompilation("class Compilation2{}", parseOptions: parseOptions); driver = driver.RunGenerators(compilation, ctx => ctx.Generator == generator1); // now run all the generators and update the compilation driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out outputCompilation, out outputDiagnostics); Assert.Empty(outputDiagnostics); } [Fact] public void GeneratorDriver_ReturnsEmptyRunResult_IfFiltered_BeforeRunning() { bool initWasCalled = false; var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen1Source.cs", c.SyntaxTrees.First().GetRoot().ToFullString() + " //generator1"); }); }).AsSourceGenerator(); var generator2 = new PipelineCallbackGenerator2((ctx) => { initWasCalled = true; ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { spc.AddSource("gen2Source.cs", c.SyntaxTrees.First().GetRoot().ToFullString() + " //generator2"); }); }).AsSourceGenerator(); var compilation = CreateCompilation("class Compilation1{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(generator1, generator2); driver = driver.RunGenerators(compilation, ctx => ctx.Generator == generator1); var result = driver.GetRunResult(); // only a single tree is generated Assert.Equal(1, result.GeneratedTrees.Length); Assert.Equal("class Compilation1{} //generator1", result.GeneratedTrees[0].ToString()); // but both generators are represented in the run results Assert.Equal(2, result.Results.Length); Assert.Equal(generator1, result.Results[0].Generator); Assert.Equal(1, result.Results[0].GeneratedSources.Length); // the second generator was never initialized and produced no results Assert.False(initWasCalled); Assert.Equal(generator2, result.Results[1].Generator); Assert.True(result.Results[1].GeneratedSources.IsDefault); } [Fact] public void GeneratorDriver_GeneratorIsNotRun_IfAlreadyUpToDate_DueToFiltering() { bool stepRan = false; var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterSourceOutput(ctx.CompilationProvider, (spc, c) => { stepRan = true; }); }).AsSourceGenerator(); var compilation = CreateCompilation("class Compilation1{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(generator1); // don't run driver = driver.RunGenerators(compilation, ctx => false); Assert.False(stepRan); // run driver = driver.RunGenerators(compilation, ctx => true); Assert.True(stepRan); // re-run everything stepRan = false; driver = driver.RunGenerators(compilation); // step didn't run because the generator was already up to date Assert.False(stepRan); } [Fact] public void ReplaceGenerators_Initializes_New_Generators() { var generator1 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(ctx => { })); bool initWasCalled = false; var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(ctx => { initWasCalled = true; ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, text) => { context.AddSource("generated", ""); }); })); var compilation = CreateCompilation("class C{}"); GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator1 }); driver = driver.RunGenerators(compilation); var results = driver.GetRunResult(); Assert.Empty(results.GeneratedTrees); driver = driver.ReplaceGenerators([generator2]); driver = driver.RunGenerators(compilation); results = driver.GetRunResult(); Assert.True(initWasCalled); Assert.Single(results.GeneratedTrees); } // check post init trees get re-parsed if we change parse options while suppressed [Fact] public void GeneratorDriver_DoesNotIncludePostInitTrees_WhenFiltered() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterPostInitializationOutput(postInitCtx => { postInitCtx.AddSource("staticSource.cs", "//static"); }); }).AsSourceGenerator(); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], parseOptions: parseOptions); driver = driver.RunGenerators(compilation, (ctx) => false); var result = driver.GetRunResult(); Assert.Empty(result.GeneratedTrees); driver = driver.RunGenerators(compilation); result = driver.GetRunResult(); Assert.Single(result.GeneratedTrees); } [Fact] public void GeneratorDriver_ReparsesPostInitTrees_IfParseOptionsChange_WhileSuppressed() { var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterPostInitializationOutput(postInitCtx => { postInitCtx.AddSource("staticSource.cs", "//static"); }); }).AsSourceGenerator(); var generator2 = new PipelineCallbackGenerator2((ctx) => { ctx.RegisterPostInitializationOutput(postInitCtx => { postInitCtx.AddSource("staticSource2.cs", "//static 2"); }); }).AsSourceGenerator(); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1, generator2], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); // change the parse options, but only run one of the generators var newParseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp9); compilation = CreateCompilation("class Compilation2{}", parseOptions: newParseOptions); driver = driver.WithUpdatedParseOptions(newParseOptions); driver = driver.RunGenerators(compilation, (ctx) => ctx.Generator == generator1); result = driver.GetRunResult(); // the post init tree from generator2 still has the old parse options, as it didn't run Assert.Equal(2, result.GeneratedTrees.Length); Assert.Same(newParseOptions, result.GeneratedTrees[0].Options); Assert.Same(parseOptions, result.GeneratedTrees[1].Options); // now run everything and ensure it brings the init trees up to date driver = driver.RunGenerators(compilation); result = driver.GetRunResult(); Assert.Equal(2, result.GeneratedTrees.Length); Assert.Same(newParseOptions, result.GeneratedTrees[0].Options); Assert.Same(newParseOptions, result.GeneratedTrees[1].Options); } #pragma warning disable RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. [Fact] public void GeneratorDriver_Makes_HostOutputs_Available() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("a", "value"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); var result = Assert.Single(runResult.Results); var (key, value) = Assert.Single(result.HostOutputs); Assert.Equal("a", key); Assert.Equal("value", value); } [Fact] public void GeneratorDriver_No_HostOutputs_WhenDisabled() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("a", "value"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create( [generator.AsSourceGenerator()], parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.Host)); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); var result = Assert.Single(runResult.Results); Assert.Empty(result.HostOutputs); } [Fact] public void GeneratorDriver_Multiple_HostOutputs() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("a", "value"); hostCtx.AddOutput("b", "value2"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); var result = Assert.Single(runResult.Results); Assert.Equal(2, result.HostOutputs.Keys.Count()); Assert.Contains("a", (IDictionary<string, object>)result.HostOutputs); Assert.Equal("value", result.HostOutputs["a"]); Assert.Contains("b", (IDictionary<string, object>)result.HostOutputs); Assert.Equal("value2", result.HostOutputs["b"]); } [Fact] public void GeneratorDriver_Multiple_HostOutputs_SameName() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("a", "value"); hostCtx.AddOutput("a", "value2"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); var result = Assert.Single(runResult.Results); Assert.Empty(result.HostOutputs); Assert.IsType<ArgumentException>(result.Exception); } [Fact] public void GeneratorDriver_Makes_HostOutputs_MultipleGenerators() { var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("gen1", "value1"); }); }); var generator2 = new PipelineCallbackGenerator2((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("gen2", "value2"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1.AsSourceGenerator(), generator2.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Collection(runResult.Results, (r) => { var result = Assert.Single(r.HostOutputs); Assert.Equal("gen1", result.Key); Assert.Equal("value1", result.Value); }, (r) => { var result = Assert.Single(r.HostOutputs); Assert.Equal("gen2", result.Key); Assert.Equal("value2", result.Value); } ); } [Fact] public void GeneratorDriver_Makes_HostOutputs_MultipleGenerators_SameName() { var generator1 = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("gen", "value1"); }); }); var generator2 = new PipelineCallbackGenerator2((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { hostCtx.AddOutput("gen", "value2"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1.AsSourceGenerator(), generator2.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); Assert.Collection(runResult.Results, (r) => { var result = Assert.Single(r.HostOutputs); Assert.Equal("gen", result.Key); Assert.Equal("value1", result.Value); }, (r) => { var result = Assert.Single(r.HostOutputs); Assert.Equal("gen", result.Key); Assert.Equal("value2", result.Value); } ); } [Fact] public void GeneratorDriver_HostOutputs_Throws() { var generator = new PipelineCallbackGenerator((ctx) => { ctx.RegisterHostOutput(ctx.CompilationProvider, (hostCtx, c) => { throw new InvalidOperationException("failed"); }); }); var parseOptions = CSharpParseOptions.Default; var compilation = CreateCompilation("class Compilation1{}", parseOptions: parseOptions); GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions); driver = driver.RunGenerators(compilation); var runResult = driver.GetRunResult(); var result = Assert.Single(runResult.Results); Assert.Empty(result.HostOutputs); var exception = Assert.IsType<InvalidOperationException>(result.Exception); Assert.Equal("failed", exception.Message); } #pragma warning restore RSEXPERIMENTAL004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } }