diff --git a/src/Directory.Build.props b/src/Directory.Build.props index defc9f9..060a4e1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,13 +2,14 @@ enable - 10 + 12 SGF true $(MSBuildThisFileDirectory) $(SGFSourceDir)libs\ $(MSBuildThisFileDirectory)..\img\ $(MSBuildThisFileDirectory)..\img\icon.png + true $(MSBuildThisFileDirectory)..\img\icon.ico diff --git a/src/SourceGenerator.Foundations.Tests/BaseTest.cs b/src/SourceGenerator.Foundations.Tests/BaseTest.cs deleted file mode 100644 index 5f49f53..0000000 --- a/src/SourceGenerator.Foundations.Tests/BaseTest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using System.Reflection; -using Xunit.Abstractions; - -namespace SourceGenerator.Foundations.Tests -{ - public class BaseTest - { - /// - /// Gets the name of the currently running test method - /// - public string TestMethodName { get; } - - protected BaseTest(ITestOutputHelper outputHelper) - { - TestMethodName = "Unknown"; - Type type = outputHelper.GetType(); - FieldInfo? testField = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); - ITest? test = testField?.GetValue(outputHelper) as ITest; - if (test != null) - { - TestMethodName = test.TestCase.TestMethod.ToString()!; - } - } - } -} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Tests/CompiliationTestBase.cs b/src/SourceGenerator.Foundations.Tests/CompiliationTestBase.cs new file mode 100644 index 0000000..f3fd231 --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/CompiliationTestBase.cs @@ -0,0 +1,155 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using Xunit.Abstractions; + +namespace SGF +{ + /// + /// There is an attribute called `ExtendedAnalyzerRules` which are applied to source generators. Since + /// SGF injects code into their projects we want to make sure we don't add warnings or errors. + /// + public class CompiliationTestBase + { + public SgfAnalyzerConfigOptions AnalyzerOptions { get; } + + /// + /// Gets the name of the currently running test method + /// + public string TestMethodName { get; } + + private readonly List m_references; + private readonly List m_analyzers; + protected readonly List m_incrementalGenerators; + + protected CompiliationTestBase(ITestOutputHelper outputHelper) + { + m_analyzers = new List(); + m_references = new List(); + m_incrementalGenerators = new List(); + + AnalyzerOptions = new SgfAnalyzerConfigOptions(); + TestMethodName = "Unkonwn"; + Type type = outputHelper.GetType(); + FieldInfo? testField = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + ITest? test = testField?.GetValue(outputHelper) as ITest; + if (test != null) + { + TestMethodName = test.TestCase.TestMethod.ToString()!; + } + + AddAssemblyReference("System.Runtime"); + AddAssemblyReference("netstandard"); + AddAssemblyReference("System.Console"); + AddAssemblyReference("Microsoft.CodeAnalysis"); + AddAssemblyReference("System.Linq"); + AddMetadataReference(); + AddMetadataReference(); + } + + + + protected async Task ComposeAsync( + string[]? source = null, + Action>[]? assertDiagnostics = null, + Action[]? assertCompilation = null) + { + + source ??= []; + + CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + + CompilationWithAnalyzersOptions analyzerOptions = new CompilationWithAnalyzersOptions( + new AnalyzerOptions([], new SgfAnalyzerConfigOptionsProvider(AnalyzerOptions)), + null, true, false); + + // Create a Roslyn compilation for the syntax tree. + Compilation compilation = CSharpCompilation.Create(TestMethodName) + .AddSyntaxTrees(source.Select(t => ParseSyntaxTree(t)).ToArray()) + .WithOptions(compilationOptions) + .AddReferences(m_references); + + // Create an instance of our EnumGenerator incremental source generator + HoistSourceGenerator generator = new HoistSourceGenerator(); + + // The GeneratorDriver is used to run our generator against a compilation + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + + // Run the source generator! + driver = driver.RunGenerators(compilation) + .RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var _); + + CompilationWithAnalyzers analysis = compilation.WithAnalyzers([.. m_analyzers], analyzerOptions); + + ImmutableArray diagnostics = await analysis.GetAllDiagnosticsAsync(); + if (assertDiagnostics is not null) + { + foreach (Action> assert in assertDiagnostics) + { + assert(diagnostics); + } + } + + if (assertCompilation is not null) + { + foreach (Action assert in assertCompilation) + { + assert(compilation); + } + } + } + + /// + /// Adds a new that will be eveluated during compliation. + /// + protected void AddAnalyzer() where T : DiagnosticAnalyzer, new() + => m_analyzers.Add(new T()); + + /// + /// Adds a new that will be eveluated during compliation. + /// + protected void AddAnalyzer(T instance) where T : DiagnosticAnalyzer + => m_analyzers.Add(instance); + + /// + /// Adds a new that will be executed during compliation. + /// + protected void AddGenerator() where T : IIncrementalGenerator, new() + => m_incrementalGenerators.Add(new T()); + + /// + /// Adds a new that will be executed during compliation. + /// + protected void AddGenerator(T instance) where T : IIncrementalGenerator + => m_incrementalGenerators.Add(instance); + + /// + /// Adds a new for the given assembly that the type belongs in + /// + protected void AddMetadataReference() + => m_references.Add(MetadataReference.CreateFromFile(typeof(T).Assembly.Location)); + + protected void AddAssemblyReference(string assemblyName) + { +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + Assembly assembly = Assembly.Load(assemblyName); + if (assembly is not null) + { + m_references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + } + + protected static SyntaxTree ParseSyntaxTree(string source, string fileName = "TestClass.cs") + { + SourceText sourceText = SourceText.From(source, Encoding.UTF8); + CSharpParseOptions parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, path: fileName); + return syntaxTree; + } + } +} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs b/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs new file mode 100644 index 0000000..16893de --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs @@ -0,0 +1,46 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using System.Text; + +namespace SourceGenerator.Foundations.Tests +{ + /// + /// Contains asserts for checking if diagnostics match expected values. + /// + internal static class DiagnosticAsserts + { + public static Action> NoErrors() + => NoneOfSeverity(DiagnosticSeverity.Error); + + public static Action> NoErrorsOrWarnings() + => NoneOfSeverity(DiagnosticSeverity.Warning, DiagnosticSeverity.Error); + + public static Action> NoneOfSeverity(params DiagnosticSeverity[] severity) + { + HashSet set = new HashSet(severity); + + return diagnostics => + { + List filtered = diagnostics + .Where(d => set.Contains(d.Severity)) + .ToList(); + + if(filtered.Count > 0) + { + StringBuilder errorBuilder = new StringBuilder(); + + foreach(var dia in filtered) + { + Location location = dia.Location; + string? filePath = Path.GetFileName(location.SourceTree?.FilePath); + + string error = $"{filePath} {location.SourceSpan.Start} {dia.Severity} {dia.Descriptor.Id}: {dia.GetMessage()}"; + + errorBuilder.AppendLine(error); + } + Assert.Fail(errorBuilder.ToString()); + } + }; + } + } +} diff --git a/src/SourceGenerator.Foundations.Tests/ExtendedAnalyzerRuleTests.cs b/src/SourceGenerator.Foundations.Tests/ExtendedAnalyzerRuleTests.cs new file mode 100644 index 0000000..7ee16da --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/ExtendedAnalyzerRuleTests.cs @@ -0,0 +1,48 @@ +using Microsoft.CodeAnalysis.CSharp.Analyzers; +using SGF; +using Xunit.Abstractions; + +namespace SourceGenerator.Foundations.Tests +{ + /// + /// Runs Roslyns analyzers to validate that code that was added + /// by the does not cause + /// errors due to `EnforceExtendedAnalyzerRules` + /// + public class ExtendedAnalyzerRuleTests : CompiliationTestBase + { + public ExtendedAnalyzerRuleTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + AnalyzerOptions.EnforceExtendedAnalyzerRules = true; + + AddGenerator(); + AddAnalyzer(); + } + + [Fact] + public Task No_Banned_Apis_In_Source() + { + string source = """ + using SGF; + + namespace Yellow + { + [SgfGenerator] + public class CustomGenerator : IncrementalGenerator + { + public CustomGenerator() : base("Generator") + {} + + public override void OnInitialize(SgfInitializationContext context) + {} + } + } + """; + + return ComposeAsync([source], + assertDiagnostics: [ + DiagnosticAsserts.NoErrors() + ]); + } + } +} diff --git a/src/SourceGenerator.Foundations.Tests/GeneratorTest.cs b/src/SourceGenerator.Foundations.Tests/GeneratorTest.cs deleted file mode 100644 index a157f09..0000000 --- a/src/SourceGenerator.Foundations.Tests/GeneratorTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using SourceGenerator.Foundations.Tests.Results; -using Xunit.Abstractions; - -namespace SourceGenerator.Foundations.Tests -{ - - public abstract class GeneratorTest : BaseTest - { - - - protected GeneratorTest(ITestOutputHelper outputHelper) : base(outputHelper) - { - } - - - - protected void Compose(string source, - params Action[] treeAsserts) where T : IIncrementalGenerator, new() - { - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); - - CSharpCompilation compilation = CSharpCompilation.Create( - assemblyName: $"{TestMethodName}", - syntaxTrees: new[] { syntaxTree }); - - T incrementalGenerator = new(); - - GeneratorDriver driver = CSharpGeneratorDriver.Create(incrementalGenerator); - - driver = driver.RunGenerators(compilation); - - - GeneratorDriverRunResult result = driver.GetRunResult(); - GeneratorDriverRunAssert assertResult = new GeneratorDriverRunAssert(result); - - foreach(Action assert in treeAsserts) - { - assert(assertResult); - } - - } - } -} diff --git a/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptions.cs b/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptions.cs new file mode 100644 index 0000000..47651ca --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptions.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace SGF +{ + + public class SgfAnalyzerConfigOptions : AnalyzerConfigOptions + { + private const string BUILD_PROP_EXTENDED_RULES = "build_property.EnforceExtendedAnalyzerRules"; + + private readonly Dictionary m_values; + + public bool EnforceExtendedAnalyzerRules + { + get => m_values.TryGetValue(BUILD_PROP_EXTENDED_RULES, out string? rawValue) && + bool.TryParse(rawValue, out bool parsedValue) && + parsedValue; + set => m_values[BUILD_PROP_EXTENDED_RULES] = value + ? "true" // has to be lower + : "false"; + } + + public SgfAnalyzerConfigOptions() + { + m_values = new Dictionary(); + + EnforceExtendedAnalyzerRules = false; + } + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + if (m_values.TryGetValue(key, out value)) + { + value ??= ""; + return true; + } + return false; + } + } +} diff --git a/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptionsProvider.cs b/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptionsProvider.cs new file mode 100644 index 0000000..cbf4fae --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/SgfAnalyzerConfigOptionsProvider.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SGF +{ + public class SgfAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider + { + public SgfAnalyzerConfigOptions SharedOptions { get; } + public override AnalyzerConfigOptions GlobalOptions => SharedOptions; + + public SgfAnalyzerConfigOptionsProvider() + { + SharedOptions = new SgfAnalyzerConfigOptions(); + } + + public SgfAnalyzerConfigOptionsProvider(SgfAnalyzerConfigOptions options) + { + SharedOptions = options; + } + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) + => GlobalOptions; + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + => GlobalOptions; + } +} diff --git a/src/SourceGenerator.Foundations.Tests/SourceGenerator.Foundations.Tests.csproj b/src/SourceGenerator.Foundations.Tests/SourceGenerator.Foundations.Tests.csproj index d4f83a1..0d549d2 100644 --- a/src/SourceGenerator.Foundations.Tests/SourceGenerator.Foundations.Tests.csproj +++ b/src/SourceGenerator.Foundations.Tests/SourceGenerator.Foundations.Tests.csproj @@ -1,15 +1,17 @@ - + net6.0 enable + 12 enable false - + + @@ -20,11 +22,19 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + - diff --git a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs index 1a637db..4e4934c 100644 --- a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs +++ b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs @@ -115,7 +115,8 @@ private static void AddAssembly(Assembly assembly) foreach (string resource in resources) { - Console.WriteLine($"Extracting {resource} assembly from {assemblyName.Name}'s resources."); + + System.Console.WriteLine($"Extracting {resource} assembly from {assemblyName.Name}'s resources."); if (TryExtractingAssembly(assembly, resource, out Assembly? loadedAssembly)) { AddAssembly(loadedAssembly!); diff --git a/src/SourceGenerator.Foundations/msbuild.binlog b/src/SourceGenerator.Foundations/msbuild.binlog new file mode 100644 index 0000000..c3879eb Binary files /dev/null and b/src/SourceGenerator.Foundations/msbuild.binlog differ