Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ Multiple source files can be added with separate `WithSource` calls.
| `BuildAs(outputKind)` | Change the compilation output kind (defaults to `DynamicallyLinkedLibrary`). |
| `SuppressCompilationErrors()` | Ignore compilation errors, useful when testing analyzers that run on code that does not compile. |
| `WithInterceptorNamespace(ns)` | Add an interceptors namespace feature flag to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary MSBuild-style build property (feature flag) to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary MSBuild-style build property (feature flag) to the compilation. The property is available through global and syntax-tree analyzer config options. |
| `WithEditorConfigOption(name, value, filename)` | Add an EditorConfig option to syntax-tree analyzer config options. Omit `filename` to apply it to all source files; specify a filename to scope it to that source file. EditorConfig options are not available through global analyzer config options. |
| `AssertDiagnostics(expectedDiagnosticIds)` | Run the analyzer and assert that the diagnostics match the `[|…|]`-marked locations. |

## Testing code fixes
Expand Down Expand Up @@ -253,7 +254,8 @@ To configure all code fix tests in a project, use `CodeFixTest.ConfigureAllCodeF
| `BuildAs(outputKind)` | Change the compilation output kind. |
| `SuppressCompilationErrors()` | Ignore compilation errors. |
| `WithInterceptorNamespace(ns)` | Add an interceptors namespace feature flag to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary build property to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary build property to the compilation. The property is available through global and syntax-tree analyzer config options. |
| `WithEditorConfigOption(name, value, filename)` | Add an EditorConfig option to syntax-tree analyzer config options. Omit `filename` to apply it to all source files; specify a filename to scope it to that source file. EditorConfig options are not available through global analyzer config options. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still on the fence by this name because roslyn doesn't really seem to care and it could very well be just different WIthProperty overloads

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "Roslyn doesn't really seem to care"?

WithEditorConfig could also work and would be shorter? But I have no problem with the current method name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you look at the underlying data structure, it seems to only understand properties that are global, properties at the file or tree level. That's it. That's why I'm wondering if WithEditorConfig or WithEditorConfigOptions should simply be "overloads of WithProperty"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflecting about this I think ConfigOptions is nice because if we ever support file loading of editorconfig then WithEditorConfig could be the file based overload.

| `AssertCodeFixes()` | Apply code fixes iteratively and assert that the final source matches the expected output. |

## Testing source generators
Expand Down Expand Up @@ -308,7 +310,8 @@ To configure all source generator tests in a project, use `SourceGeneratorTest.C
| `SuppressCompilationErrors()` | Ignore compilation warnings and errors. |
| `SuppressDiagnosticErrors()` | Ignore errors raised by the source generator itself. |
| `WithInterceptorNamespace(ns)` | Add an interceptors namespace feature flag to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary build property to the compilation. |
| `WithProperty(name, value)` | Add an arbitrary build property to the compilation. The property is available through global and syntax-tree analyzer config options. |
| `WithEditorConfigOption(name, value, filename)` | Add an EditorConfig option to syntax-tree analyzer config options. Omit `filename` to apply it to all source files; specify a filename to scope it to that source file. EditorConfig options are not available through global analyzer config options. |
| `Run()` | Run the source generator without running an approval test. |
| `Approve(scrubber)` | Run the generator (if not already run) and perform an approval test on the generated output. |
| `ShouldNotGenerateCode()` | Assert that the source generator produces no output for the given sources. |
Expand Down
8 changes: 8 additions & 0 deletions src/FakeAnalyzers/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ public static class DiagnosticDescriptors
category: "Code",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor EditorConfigOptionEnabled = new(
id: DiagnosticIds.EditorConfigOptionEnabled,
title: "EditorConfig option is enabled",
messageFormat: "The EditorConfig option is enabled for '{0}'",
category: "Code",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
1 change: 1 addition & 0 deletions src/FakeAnalyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public static class DiagnosticIds
public const string AsyncVoid = "FAKE0002";
public const string IdentifierContainsFoo = "FAKE0003";
public const string TestFlagEnabled = "FAKE0004";
public const string EditorConfigOptionEnabled = "FAKE0005";
}
35 changes: 35 additions & 0 deletions src/FakeAnalyzers/EditorConfigOptionAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace FakeAnalyzers;

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class EditorConfigOptionAnalyzer : DiagnosticAnalyzer
{
const string OptionName = "nservicebus_enable_message_overload_migration_diagnostics";

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [DiagnosticDescriptors.EditorConfigOptionEnabled];

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration);
}

static void AnalyzeClass(SyntaxNodeAnalysisContext context)
{
var optionsProvider = context.Options.AnalyzerConfigOptionsProvider;
if (!optionsProvider.GetOptions(context.Node.SyntaxTree).TryGetValue(OptionName, out var value) || value != "true" || optionsProvider.GlobalOptions.TryGetValue(OptionName, out _))
{
return;
}

var classDeclaration = (ClassDeclarationSyntax)context.Node;
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.EditorConfigOptionEnabled, classDeclaration.Identifier.GetLocation(), classDeclaration.Identifier.Text);
context.ReportDiagnostic(diagnostic);
}
}
34 changes: 34 additions & 0 deletions src/FakeAnalyzers/EditorConfigOptionSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace FakeAnalyzers;

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

[Generator(LanguageNames.CSharp)]
public sealed class EditorConfigOptionSourceGenerator : IIncrementalGenerator
{
const string AllSourceOption = "test_all_source_option";
const string FilenameOption = "test_filename_option";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var compilationAndOptions = context.CompilationProvider.Combine(context.AnalyzerConfigOptionsProvider);
context.RegisterSourceOutput(compilationAndOptions, static (productionContext, input) =>
{
var output = new StringBuilder();

foreach (var tree in input.Left.SyntaxTrees)
{
var options = input.Right.GetOptions(tree);
var allSourceValue = GetOptionValue(options, AllSourceOption);
var filenameValue = GetOptionValue(options, FilenameOption);
output.AppendLine($"// {tree.FilePath}: all-source={allSourceValue}; filename={filenameValue}");
}

productionContext.AddSource("EditorConfigOptions.g.cs", output.ToString());
});
}

static string GetOptionValue(AnalyzerConfigOptions options, string name)
=> options.TryGetValue(name, out var value) ? value : "<not-set>";
}
4 changes: 3 additions & 1 deletion src/FakeAnalyzers/TestFlagAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public override void Initialize(AnalysisContext context)

static void AnalyzeClass(SyntaxNodeAnalysisContext context)
{
if (context.Node is not ClassDeclarationSyntax classDeclaration)
if (context.Node is not ClassDeclarationSyntax classDeclaration ||
!context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Node.SyntaxTree).TryGetValue("build_property.TestFlag", out var value) ||
value != "enabled")
{
return;
}
Expand Down
47 changes: 39 additions & 8 deletions src/Particular.AnalyzerTesting/AnalyzerConfigOptionsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,48 @@ namespace Particular.AnalyzerTesting;

static class AnalyzerConfigOptionsFactory
{
public static AnalyzerConfigOptionsProvider CreateOptionsProvider(IReadOnlyDictionary<string, string> properties)
=> new OptionsProvider(new DictionaryAnalyzerConfigOptions(properties));
public static AnalyzerConfigOptionsProvider CreateOptionsProvider(
IReadOnlyDictionary<string, string> globalProperties,
IReadOnlyDictionary<string, string>? sourceProperties = null,
IReadOnlyDictionary<string, Dictionary<string, string>>? sourceFileProperties = null)
=> new OptionsProvider(globalProperties, sourceProperties ?? new Dictionary<string, string>(), sourceFileProperties ?? new Dictionary<string, Dictionary<string, string>>());

public static AnalyzerOptions CreateAnalyzerOptions(IReadOnlyDictionary<string, string> properties)
=> new([], CreateOptionsProvider(properties));
public static AnalyzerOptions CreateAnalyzerOptions(
IReadOnlyDictionary<string, string> globalProperties,
IReadOnlyDictionary<string, string>? sourceProperties = null,
IReadOnlyDictionary<string, Dictionary<string, string>>? sourceFileProperties = null)
=> new([], CreateOptionsProvider(globalProperties, sourceProperties, sourceFileProperties));

sealed class OptionsProvider(AnalyzerConfigOptions options) : AnalyzerConfigOptionsProvider
sealed class OptionsProvider(
IReadOnlyDictionary<string, string> globalProperties,
IReadOnlyDictionary<string, string> sourceProperties,
IReadOnlyDictionary<string, Dictionary<string, string>> sourceFileProperties) : AnalyzerConfigOptionsProvider
{
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => options;
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => options;
public override AnalyzerConfigOptions GlobalOptions => options;
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
{
var properties = new Dictionary<string, string>(globalProperties);
AddProperties(properties, sourceProperties);

if (sourceFileProperties.TryGetValue(tree.FilePath, out var fileProperties))
{
AddProperties(properties, fileProperties);
}

return new DictionaryAnalyzerConfigOptions(properties);
}

public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
=> new DictionaryAnalyzerConfigOptions(globalProperties);

public override AnalyzerConfigOptions GlobalOptions { get; } = new DictionaryAnalyzerConfigOptions(globalProperties);

static void AddProperties(Dictionary<string, string> destination, IReadOnlyDictionary<string, string> source)
{
foreach (var (key, value) in source)
{
destination[key] = value;
}
}
}

sealed class DictionaryAnalyzerConfigOptions(IReadOnlyDictionary<string, string> properties) : AnalyzerConfigOptions
Expand Down
2 changes: 1 addition & 1 deletion src/Particular.AnalyzerTesting/BaseAnalyzerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private protected static async Task<Diagnostic[]> GetCompilerDiagnostics(Project
private protected async Task<Diagnostic[]> GetAnalyzerDiagnostics(Compilation compilation, string[] ignoreDiagnosticIds, CancellationToken cancellationToken = default)
{
var analyzerTasks = analyzers
.Select(analyzer => compilation.GetAnalyzerDiagnostics(analyzer, features, cancellationToken))
.Select(analyzer => compilation.GetAnalyzerDiagnostics(analyzer, features, editorConfigOptions, editorConfigOptionsByFilename, cancellationToken))
.ToArray();

await Task.WhenAll(analyzerTasks);
Expand Down
27 changes: 26 additions & 1 deletion src/Particular.AnalyzerTesting/BaseCompilationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public abstract class BaseCompilationTest<TSelf> where TSelf : BaseCompilationTe
private protected OutputKind buildOutputType = OutputKind.DynamicallyLinkedLibrary;
private protected bool suppressCompilationErrors;
private protected readonly Dictionary<string, string> features = [];
private protected readonly Dictionary<string, string> editorConfigOptions = [];
private protected readonly Dictionary<string, Dictionary<string, string>> editorConfigOptionsByFilename = [];

private protected BaseCompilationTest(string? outputAssemblyName = null)
{
Expand Down Expand Up @@ -128,11 +130,34 @@ public TSelf WithInterceptorNamespace(string interceptorNamespace)
}

/// <summary>
/// Add a build property to the compilation.
/// Add a build property to the compilation. The property is available through both the global and syntax-tree analyzer config options.
/// </summary>
public TSelf WithProperty(string name, string value)
{
features.Add(name, value);
return Self;
}

/// <summary>
/// Add an EditorConfig option that is available through syntax-tree analyzer config options.
/// Omit <paramref name="filename" /> to apply the option to every source file.
/// The option is not added to global analyzer config options.
/// </summary>
public TSelf WithEditorConfigOption(string name, string value, string? filename = null)
{
if (filename is null)
{
editorConfigOptions.Add(name, value);
return Self;
}

if (!editorConfigOptionsByFilename.TryGetValue(filename, out var fileOptions))
{
fileOptions = [];
editorConfigOptionsByFilename.Add(filename, fileOptions);
}

fileOptions.Add(name, value);
return Self;
}
}
9 changes: 7 additions & 2 deletions src/Particular.AnalyzerTesting/CompilationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,17 @@ public void Compile(bool throwOnFailure = true)
Debug.WriteLine("Compilation failed.");
}

public async Task<IEnumerable<Diagnostic>> GetAnalyzerDiagnostics(DiagnosticAnalyzer analyzer, IReadOnlyDictionary<string, string> properties, CancellationToken cancellationToken = default)
public async Task<IEnumerable<Diagnostic>> GetAnalyzerDiagnostics(
DiagnosticAnalyzer analyzer,
IReadOnlyDictionary<string, string> globalProperties,
IReadOnlyDictionary<string, string> sourceProperties,
IReadOnlyDictionary<string, Dictionary<string, string>> sourceFileProperties,
CancellationToken cancellationToken = default)
{
var exceptions = new List<Exception>();

var analysisOptions = new CompilationWithAnalyzersOptions(
AnalyzerConfigOptionsFactory.CreateAnalyzerOptions(properties),
AnalyzerConfigOptionsFactory.CreateAnalyzerOptions(globalProperties, sourceProperties, sourceFileProperties),
(exception, _, __) => exceptions.Add(exception),
concurrentAnalysis: false,
logAnalyzerExecutionTime: false);
Expand Down
2 changes: 1 addition & 1 deletion src/Particular.AnalyzerTesting/SourceGeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public SourceGeneratorTestResult Run()
disabledOutputs: IncrementalGeneratorOutputKind.None,
trackIncrementalGeneratorSteps: true);

var optsProvider = AnalyzerConfigOptionsFactory.CreateOptionsProvider(features);
var optsProvider = AnalyzerConfigOptionsFactory.CreateOptionsProvider(features, editorConfigOptions, editorConfigOptionsByFilename);
Comment thread
danielmarbach marked this conversation as resolved.

var driver = CSharpGeneratorDriver.Create(generators,
driverOptions: driverOpts,
Expand Down
16 changes: 16 additions & 0 deletions src/Tests/Analyzers/TestFlagAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,20 @@ public Task ReportsDiagnosticWhenPropertyIsEnabled() =>
.WithProperty("build_property.TestFlag", "enabled")
.WithSource(Code)
.AssertDiagnostics(DiagnosticIds.TestFlagEnabled);

[Test]
public Task ReportsDiagnosticWhenEditorConfigOptionIsEnabledForAllSources() =>
AnalyzerTest.ForAnalyzer<EditorConfigOptionAnalyzer>()
.WithEditorConfigOption("nservicebus_enable_message_overload_migration_diagnostics", "true")
.WithSource("public class [|First|] { }", "First.cs")
.WithSource("public class [|Second|] { }", "Second.cs")
.AssertDiagnostics(DiagnosticIds.EditorConfigOptionEnabled);

[Test]
public Task ReportsDiagnosticOnlyForEditorConfigOptionSourceFile() =>
AnalyzerTest.ForAnalyzer<EditorConfigOptionAnalyzer>()
.WithEditorConfigOption("nservicebus_enable_message_overload_migration_diagnostics", "true", "Selected.cs")
.WithSource("public class [|Selected|] { }", "Selected.cs")
.WithSource("public class Other { }", "Other.cs")
.AssertDiagnostics(DiagnosticIds.EditorConfigOptionEnabled);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ namespace Particular.AnalyzerTesting
public TSelf SuppressCompilationErrors() { }
public TSelf WithAnalyzer<TAnalyzer>()
where TAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new () { }
public TSelf WithEditorConfigOption(string name, string value, string? filename = null) { }
public TSelf WithInterceptorNamespace(string interceptorNamespace) { }
public TSelf WithLangVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion langVersion) { }
public TSelf WithProperty(string name, string value) { }
Expand Down
15 changes: 15 additions & 0 deletions src/Tests/SourceGenerators/BasicSourceGeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,19 @@ public void AnalyzerSeesPropertyDuringSourceGeneratorRun()

Assert.That(result.AnalyzerDiagnostics.Select(diagnostic => diagnostic.Id), Contains.Item(DiagnosticIds.TestFlagEnabled));
}

[Test]
public void SourceGeneratorSeesEditorConfigOptionsForEachSourceFile()
{
var result = SourceGeneratorTest.ForIncrementalGenerator<EditorConfigOptionSourceGenerator>(["CompilationAndOptions"])
.WithEditorConfigOption("test_all_source_option", "all-sources")
.WithEditorConfigOption("test_filename_option", "selected-only", "Selected.cs")
.WithSource("public class Selected { }", "Selected.cs")
.WithSource("public class Other { }", "Other.cs")
.Run();

var output = result.GetCompilationOutput();
Assert.That(output, Does.Contain("// Selected.cs: all-source=all-sources; filename=selected-only"));
Assert.That(output, Does.Contain("// Other.cs: all-source=all-sources; filename=<not-set>"));
}
}