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
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ Every `RoslynFixtureFactory.Create<T>()` overload accepts an optional config obj
| `ThrowsWhenInputDocumentContainsError` | `bool` | `true` | Throw when the code under test has compiler errors. |
| `References` | `IReadOnlyList<MetadataReference>` | empty | Extra metadata references added to the project. |
| `AdditionalFiles` | `IReadOnlyList<AdditionalText>` | empty | Additional files exposed to analyzers (e.g. `.editorconfig`). |
| `RuleSetPath` | `string?` | `null` | Path to a `.ruleset` file that controls diagnostic severity. |
| `RuleSetPath` | `string?` | `null` | Path to a JSON ruleset file. Its general and per-rule diagnostic options are applied to the compilation, so it can change severities and **enable diagnostics that are `isEnabledByDefault: false`**. |
| `PackageCachePaths` | `IReadOnlyList<string>` | empty | Directories containing `.app` packages the compiler resolves symbols from. |
| `CompilationOptions` | `CompilationOptions?` | `null` | Override the default `CompilationOptions`. |
| `ParseOptions` | `ParseOptions?` | `null` | Override the default `ParseOptions`. |
Expand All @@ -338,11 +338,26 @@ Every `RoslynFixtureFactory.Create<T>()` overload accepts an optional config obj

#### Example: apply a ruleset to an analyzer test

A ruleset is a JSON file. Its `action` values map to diagnostic severities
(`Error`, `Warning`, `Info`, `Hidden`, `None`, `Default`). Setting a real
severity also **enables a rule that is `isEnabledByDefault: false`**, which is the
typical reason to use a ruleset in tests:

```json
{
"name": "Enable FlowFieldsShouldNotBeEditable",
"description": "Enables the rule for tests.",
"rules": [
{ "id": "LC0001", "action": "Warning" }
]
}
```

```csharp
var fixture = RoslynFixtureFactory.Create<FlowFieldsShouldNotBeEditable>(
new AnalyzerTestFixtureConfig
{
RuleSetPath = @"rules\my.ruleset"
RuleSetPath = @"rules\my.ruleset.json"
});

fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable);
Expand Down
29 changes: 29 additions & 0 deletions src/RoslynTestKit/BaseTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions;
using Document = Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.Document;
using LanguageNames = Microsoft.Dynamics.Nav.CodeAnalysis.LanguageNames;
using NavDiagnostic = Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics.Diagnostic;
using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions;
using RuleSetResolver = Microsoft.Dynamics.Nav.CodeAnalysis.DiagnosticRules.RuleSetResolver;

namespace RoslynTestKit
{
Expand Down Expand Up @@ -75,6 +77,7 @@
var frameworkReferences = CreateFrameworkMetadataReferences();

var compilationOptions = CustomCompilationOptions ?? GetCompilationOptions(languageName);
compilationOptions = ApplyRuleSet(compilationOptions);

var settings = new ProjectSettings
{
Expand All @@ -90,7 +93,7 @@
throw new ArgumentException("Code cannot be empty after splitting", nameof(code));
}

var project = new AdhocWorkspace()

Check warning on line 96 in src/RoslynTestKit/BaseTestFixture.cs

View workflow job for this annotation

GitHub Actions / Build, Pack and Validate

Dereference of a possibly null reference.

Check warning on line 96 in src/RoslynTestKit/BaseTestFixture.cs

View workflow job for this annotation

GitHub Actions / Build, Pack and Validate

Dereference of a possibly null reference.
.AddProject("TestProject", languageName, settings)
.WithCompilationOptions(compilationOptions)
.AddMetadataReferences(frameworkReferences)
Expand All @@ -116,6 +119,32 @@
_ => throw new NotSupportedException($"Language {languageName} is not supported")
};

/// <summary>
/// Loads the ruleset referenced by <see cref="RuleSetPath"/> (if any) and applies its
/// general and per-rule diagnostic options to the supplied <paramref name="options"/>.
/// This is what actually enables/disables diagnostics in tests; the platform does not apply
/// the ruleset to the compilation automatically when projects are built in-memory.
/// </summary>
private CompilationOptions ApplyRuleSet(CompilationOptions options)
{
if (string.IsNullOrEmpty(RuleSetPath))
{
return options;
}

var diagnostics = new List<NavDiagnostic>();
var ruleSet = RuleSetResolver.LoadFromFile(RuleSetPath, diagnostics, externalRulesetsEnabled: false);

if (diagnostics.Count > 0)
{
throw RoslynTestKitException.InvalidRuleSet(RuleSetPath, diagnostics);
}

return options
.WithGeneralDiagnosticOption(ruleSet.GeneralDiagnosticOption)
.WithSpecificDiagnosticOptions(ruleSet.SpecificDiagnosticOptions);
}

protected virtual IEnumerable<MetadataReference> CreateFrameworkMetadataReferences()
{
yield return ReferenceSource.Core;
Expand Down
7 changes: 7 additions & 0 deletions src/RoslynTestKit/RoslynTestKitException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,12 @@ public static RoslynTestKitException FixAllReturnedNoAction(string? equivalenceK
$"FixAllProvider.GetFixAsync() returned null{keyInfo}. " +
"The FixAll operation produced no code action.");
}

public static RoslynTestKitException InvalidRuleSet(string ruleSetPath, IReadOnlyList<Diagnostic> diagnostics)
{
var details = diagnostics.MergeWithNewLines(d => $" [{d.Id}] {d.GetMessage()}");
return new RoslynTestKitException(
$"Failed to load ruleset '{ruleSetPath}'.\r\n{details}");
}
}
}
Loading