diff --git a/README.md b/README.md index f6b584d..06c8dcf 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ Every `RoslynFixtureFactory.Create()` overload accepts an optional config obj | `ThrowsWhenInputDocumentContainsError` | `bool` | `true` | Throw when the code under test has compiler errors. | | `References` | `IReadOnlyList` | empty | Extra metadata references added to the project. | | `AdditionalFiles` | `IReadOnlyList` | 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` | empty | Directories containing `.app` packages the compiler resolves symbols from. | | `CompilationOptions` | `CompilationOptions?` | `null` | Override the default `CompilationOptions`. | | `ParseOptions` | `ParseOptions?` | `null` | Override the default `ParseOptions`. | @@ -338,11 +338,26 @@ Every `RoslynFixtureFactory.Create()` 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( new AnalyzerTestFixtureConfig { - RuleSetPath = @"rules\my.ruleset" + RuleSetPath = @"rules\my.ruleset.json" }); fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.FlowFieldsShouldNotBeEditable); diff --git a/src/RoslynTestKit/BaseTestFixture.cs b/src/RoslynTestKit/BaseTestFixture.cs index 2c52075..857bcca 100644 --- a/src/RoslynTestKit/BaseTestFixture.cs +++ b/src/RoslynTestKit/BaseTestFixture.cs @@ -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 { @@ -75,6 +77,7 @@ protected virtual Document CreateDocumentFromCode(string code, string languageNa var frameworkReferences = CreateFrameworkMetadataReferences(); var compilationOptions = CustomCompilationOptions ?? GetCompilationOptions(languageName); + compilationOptions = ApplyRuleSet(compilationOptions); var settings = new ProjectSettings { @@ -116,6 +119,32 @@ private static CompilationOptions GetCompilationOptions(string languageName) => _ => throw new NotSupportedException($"Language {languageName} is not supported") }; + /// + /// Loads the ruleset referenced by (if any) and applies its + /// general and per-rule diagnostic options to the supplied . + /// 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. + /// + private CompilationOptions ApplyRuleSet(CompilationOptions options) + { + if (string.IsNullOrEmpty(RuleSetPath)) + { + return options; + } + + var diagnostics = new List(); + 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 CreateFrameworkMetadataReferences() { yield return ReferenceSource.Core; diff --git a/src/RoslynTestKit/RoslynTestKitException.cs b/src/RoslynTestKit/RoslynTestKitException.cs index cc425cd..c1ab5c0 100644 --- a/src/RoslynTestKit/RoslynTestKitException.cs +++ b/src/RoslynTestKit/RoslynTestKitException.cs @@ -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 diagnostics) + { + var details = diagnostics.MergeWithNewLines(d => $" [{d.Id}] {d.GetMessage()}"); + return new RoslynTestKitException( + $"Failed to load ruleset '{ruleSetPath}'.\r\n{details}"); + } } } \ No newline at end of file