From ae66ef58e4a50e55ebee1d19fb5d17e7fb4975c7 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 18 Apr 2026 16:54:12 +0200 Subject: [PATCH 1/2] feat: add IFileSystem support to test fixtures Add FileSystem property to BaseTestFixtureConfig, allowing tests to inject a virtual file system (e.g. MemoryFileSystem) into the compilation. This enables testing analyzers that read workspace files such as XLIFF translations or configuration files. Changes: - BaseTestFixtureConfig: new IFileSystem? FileSystem property - BaseTestFixture: virtual FileSystem property - AnalyzerTestFixture: inject via compilation.WithFileSystem() in GetDiagnostics() - CodeFixTestFixture: inject via compilation.WithFileSystem() in GetAllReportedDiagnostics() - All Configurable* fixtures: wire FileSystem from config - README.md: document FileSystem option with MemoryFileSystem example - Add filesystem-support instruction file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../filesystem-support.instructions.md | 84 +++++++++++++++++++ README.md | 34 ++++++++ src/RoslynTestKit/AnalyzerTestFixture.cs | 5 ++ src/RoslynTestKit/BaseTestFixture.cs | 7 ++ src/RoslynTestKit/BaseTestFixtureConfig.cs | 9 ++ src/RoslynTestKit/CodeFixTestFixture.cs | 5 ++ .../ConfigurableAnalyzerTestFixture.cs | 2 + .../ConfigurableCodeFixTestFixture.cs | 2 + .../ConfigurableCodeRefactoringTestFixture.cs | 2 + ...nfigurableCompletionProviderTestFixture.cs | 2 + 10 files changed, 152 insertions(+) create mode 100644 .github/instructions/filesystem-support.instructions.md diff --git a/.github/instructions/filesystem-support.instructions.md b/.github/instructions/filesystem-support.instructions.md new file mode 100644 index 0000000..ee0c904 --- /dev/null +++ b/.github/instructions/filesystem-support.instructions.md @@ -0,0 +1,84 @@ +--- +applyTo: 'src/RoslynTestKit/**' +--- + +# FileSystem Support in RoslynTestKit + +## Purpose + +Some AL analyzers depend on `Compilation.FileSystem` to read workspace files at analysis time (e.g. XLIFF translation files, configuration files). The `FileSystem` config property lets tests inject a virtual file system into the compilation so these analyzers can run without touching disk. + +## How it works + +1. `BaseTestFixtureConfig` exposes `IFileSystem? FileSystem` (default: `null`). +2. All `Configurable*` fixture classes wire this property from the config to the base fixture. +3. `AnalyzerTestFixture.GetDiagnostics()` and `CodeFixTestFixture.GetAllReportedDiagnostics()` call `compilation.WithFileSystem(FileSystem)` before creating `CompilationWithAnalyzers`, replacing any existing file system on the compilation. +4. When `FileSystem` is `null`, no replacement occurs and the compilation retains its default (typically `null`). + +## SDK's MemoryFileSystem + +The `Microsoft.Dynamics.Nav.CodeAnalysis` SDK includes a public `MemoryFileSystem` class: + +```csharp +public class MemoryFileSystem : IFileSystem +{ + public MemoryFileSystem(IDictionary files); + public string GetDirectoryPath(); // always returns "" + public IEnumerable GetFiles(string glob); + public Stream OpenRead(string path); +} +``` + +Key behaviors: +- Constructor uses `PackagePathComparer.Instance` internally (case-insensitive on Windows). +- `GetDirectoryPath()` returns `""` (empty string). Analyzers that use this for path resolution get an empty workspace path. +- `GetFiles(string glob)` supports single-parameter glob matching (e.g. `Translations/*.xlf`). Do NOT use the two-parameter overload as it may not behave the same way. +- Keys should use forward slashes: `Translations/MyApp.da-DK.xlf`. + +## Usage pattern in tests + +```csharp +var xliffContent = """ + + + + + + +"""; + +var files = new Dictionary +{ + ["Translations/TestApp.da-DK.xlf"] = Encoding.UTF8.GetBytes(xliffContent) +}; + +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + FileSystem = new MemoryFileSystem(files) + }); +``` + +## Design decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Property location | `BaseTestFixtureConfig` | All fixture types (analyzer, codefix, refactoring, completion) may need FileSystem | +| Null default | `null` | Backward compatible; only analyzers that need FileSystem will use it | +| Injection point | Before `CompilationWithAnalyzers` creation | FileSystem must be set before analyzer callbacks fire | +| No custom MemoryFileSystem | Use SDK's built-in class | Avoids maintenance burden; SDK class is public and stable | + +## Affected files + +- `BaseTestFixtureConfig.cs`: `IFileSystem? FileSystem` property declaration +- `BaseTestFixture.cs`: Virtual `IFileSystem? FileSystem` property +- `AnalyzerTestFixture.cs`: `compilation.WithFileSystem(FileSystem)` in `GetDiagnostics()` +- `CodeFixTestFixture.cs`: `compilation.WithFileSystem(FileSystem)` in `GetAllReportedDiagnostics()` +- `ConfigurableAnalyzerTestFixture.cs`: Wires from config +- `ConfigurableCodeFixTestFixture.cs`: Wires from config +- `ConfigurableCodeRefactoringTestFixture.cs`: Wires from config +- `ConfigurableCompletionProviderTestFixture.cs`: Wires from config + +## Known issues + +- `ManifestHelper.GetManifest(compilation)` in ALCops.Common throws `FileNotFoundException` for `Microsoft.Dynamics.Nav.Analyzers.Common` in test contexts because that assembly isn't available. Analyzers using `ManifestHelper` must wrap the call in try-catch. This is NOT a RoslynTestKit issue but affects analyzers that use FileSystem-dependent features alongside ManifestHelper. diff --git a/README.md b/README.md index 0f2eca5..4887491 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,7 @@ Every `RoslynFixtureFactory.Create()` overload accepts an optional config obj | `CompilationOptions` | `CompilationOptions?` | `null` | Override the default `CompilationOptions`. | | `ParseOptions` | `ParseOptions?` | `null` | Override the default `ParseOptions`. | | `ProjectInfoCustomizer` | `Func?` | `null` | Escape hatch to set any `ProjectInfo` property not exposed above. | +| `FileSystem` | `IFileSystem?` | `null` | Virtual file system injected into the compilation via `Compilation.WithFileSystem()`. Use for analyzers that read files from the workspace (e.g. XLIFF translations, config files). | #### CodeFixTestFixtureConfig extras @@ -283,6 +284,39 @@ var fixture = RoslynFixtureFactory.Create + + + + + +"""; + +var files = new Dictionary +{ + ["Translations/MyApp.da-DK.xlf"] = Encoding.UTF8.GetBytes(xliff) +}; + +var fixture = RoslynFixtureFactory.Create( + new AnalyzerTestFixtureConfig + { + FileSystem = new MemoryFileSystem(files) + }); + +fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.MissingTranslation); +``` + +> **Note:** `MemoryFileSystem.GetDirectoryPath()` always returns `""`. Keys should use forward slashes and match the glob patterns your analyzer passes to `IFileSystem.GetFiles()` (e.g. `Translations/*.xlf`). + #### Example: escape hatch for advanced ProjectInfo settings When you need to configure a `ProjectInfo` property that has no dedicated config option, use `ProjectInfoCustomizer`. diff --git a/src/RoslynTestKit/AnalyzerTestFixture.cs b/src/RoslynTestKit/AnalyzerTestFixture.cs index d204dee..8690170 100644 --- a/src/RoslynTestKit/AnalyzerTestFixture.cs +++ b/src/RoslynTestKit/AnalyzerTestFixture.cs @@ -141,6 +141,11 @@ private ImmutableArray GetDiagnostics(Document document) return ImmutableArray.Empty; } + if (FileSystem != null) + { + compilation = compilation.WithFileSystem(FileSystem); + } + var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, options: AdditionalFiles != null ? new AnalyzerOptions(AdditionalFiles.ToImmutableArray()) : null, cancellationToken: CancellationToken.None); var discarded = compilation.GetDiagnostics(CancellationToken.None); var errorsInDocument = discarded.Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); diff --git a/src/RoslynTestKit/BaseTestFixture.cs b/src/RoslynTestKit/BaseTestFixture.cs index bbaa2c0..2c52075 100644 --- a/src/RoslynTestKit/BaseTestFixture.cs +++ b/src/RoslynTestKit/BaseTestFixture.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; @@ -52,6 +53,12 @@ public abstract class BaseTestFixture /// protected virtual Func? ProjectInfoCustomizer => null; + /// + /// Optional injected into the before analyzers + /// run. When non-null, the compilation is recreated via Compilation.WithFileSystem(). + /// + protected virtual IFileSystem? FileSystem => null; + protected Document CreateDocumentFromCode(string code) { return CreateDocumentFromCode(code, LanguageName, References ?? Array.Empty()); diff --git a/src/RoslynTestKit/BaseTestFixtureConfig.cs b/src/RoslynTestKit/BaseTestFixtureConfig.cs index 91fde6a..b673163 100644 --- a/src/RoslynTestKit/BaseTestFixtureConfig.cs +++ b/src/RoslynTestKit/BaseTestFixtureConfig.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; @@ -48,5 +49,13 @@ public abstract class BaseTestFixtureConfig /// directly on the config. /// public Func? ProjectInfoCustomizer { get; set; } = null; + + /// + /// Optional injected into the before analyzers + /// run. Use the SDK's to provide in-memory files (e.g. XLIFF + /// translation files) that analyzers read via Compilation.FileSystem. + /// When null, the compilation keeps whatever file system the workspace assigns (typically none). + /// + public IFileSystem? FileSystem { get; set; } = null; } } \ No newline at end of file diff --git a/src/RoslynTestKit/CodeFixTestFixture.cs b/src/RoslynTestKit/CodeFixTestFixture.cs index 275c588..cbe71b6 100644 --- a/src/RoslynTestKit/CodeFixTestFixture.cs +++ b/src/RoslynTestKit/CodeFixTestFixture.cs @@ -178,6 +178,11 @@ private IEnumerable GetAllReportedDiagnostics(Document document) throw new InvalidOperationException("Unable to get compilation from document project."); } + if (FileSystem != null) + { + compilation = compilation.WithFileSystem(FileSystem); + } + return compilation .WithAnalyzers(additionalAnalyzers.ToImmutableArray(), new AnalyzerOptions(this.AdditionalFiles?.ToImmutableArray() ?? ImmutableArray.Empty)) .GetAnalyzerDiagnosticsAsync().GetAwaiter().GetResult() diff --git a/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs b/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs index 94914a2..ae84bac 100644 --- a/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableAnalyzerTestFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; @@ -30,5 +31,6 @@ public ConfigurableAnalyzerTestFixture(DiagnosticAnalyzer diagnosticAnalyzer, An protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; protected override ParseOptions? ParseOptions => _config.ParseOptions; protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; + protected override IFileSystem? FileSystem => _config.FileSystem; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs b/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs index 134eabb..844a67b 100644 --- a/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCodeFixTestFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.CodeFixes; using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; @@ -32,5 +33,6 @@ public ConfigurableCodeFixTestFixture(CodeFixProvider provider, CodeFixTestFixtu protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; protected override ParseOptions? ParseOptions => _config.ParseOptions; protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; + protected override IFileSystem? FileSystem => _config.FileSystem; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs b/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs index 4810deb..88712b7 100644 --- a/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCodeRefactoringTestFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.CodeRefactoring; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; @@ -30,5 +31,6 @@ public ConfigurableCodeRefactoringTestFixture(CodeRefactoringProvider provider, protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; protected override ParseOptions? ParseOptions => _config.ParseOptions; protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; + protected override IFileSystem? FileSystem => _config.FileSystem; } } \ No newline at end of file diff --git a/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs b/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs index 02ff96f..7c3ddb4 100644 --- a/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs +++ b/src/RoslynTestKit/ConfigurableCompletionProviderTestFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.Completion; using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; @@ -30,5 +31,6 @@ public ConfigurableCompletionProviderTestFixture(CompletionProvider provider, Co protected override CompilationOptions? CustomCompilationOptions => _config.CompilationOptions; protected override ParseOptions? ParseOptions => _config.ParseOptions; protected override Func? ProjectInfoCustomizer => _config.ProjectInfoCustomizer; + protected override IFileSystem? FileSystem => _config.FileSystem; } } \ No newline at end of file From 49a21858b6f94a3b34d20a410897cb5dc0785570 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sat, 18 Apr 2026 16:54:42 +0200 Subject: [PATCH 2/2] feat: add Copilot instructions for RoslynTestKit --- .github/copilot-instructions.md | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7f2ccc7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,86 @@ +# Copilot Instructions for RoslynTestKit + +## What This Is + +A NuGet library (`ALCops.RoslynTestKit`) for unit-testing Roslyn-based diagnostic analyzers, code fixes, refactorings, and completion providers targeting the **AL Language** (Microsoft Dynamics 365 Business Central). Forked from [cezarypiatek/RoslynTestKit](https://github.com/cezarypiatek/RoslynTestKit), it replaces `Microsoft.CodeAnalysis` types with their `Microsoft.Dynamics.Nav.CodeAnalysis` equivalents. + +## Build + +```bash +dotnet restore src/RoslynTestKit.sln +dotnet build src/RoslynTestKit.sln --configuration Release +dotnet pack src/RoslynTestKit/RoslynTestKit.csproj --configuration Release --output ./artifacts +``` + +There is no test project in this repository. The library itself is the deliverable. + +The project multi-targets `netstandard2.1` and `net8.0`. Each TFM references a different version of the BC DevTools DLLs (`Microsoft.Dynamics.Nav.CodeAnalysis.dll` and `Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.dll`) from a local path. CI downloads these via the custom `.github/actions/setup-bc-devtools` composite action. + +## Architecture + +### Namespace aliasing pattern + +Throughout the codebase, `Microsoft.Dynamics.Nav.CodeAnalysis` types shadow their `Microsoft.CodeAnalysis` counterparts via `using` aliases. This is intentional and central to the design: + +```csharp +using AdditionalText = Microsoft.Dynamics.Nav.CodeAnalysis.AdditionalText; +using CompilationOptions = Microsoft.Dynamics.Nav.CodeAnalysis.CompilationOptions; +using Document = Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.Document; +using LanguageNames = Microsoft.Dynamics.Nav.CodeAnalysis.LanguageNames; +using ParseOptions = Microsoft.Dynamics.Nav.CodeAnalysis.ParseOptions; +``` + +When adding or modifying code, always use the Nav.CodeAnalysis types, not the Microsoft.CodeAnalysis originals, unless there is no Nav equivalent (e.g., `MetadataReference`). + +### Fixture hierarchy + +``` +BaseTestFixture (abstract, creates AdhocWorkspace + Document from code) +├── AnalyzerTestFixture (abstract, HasDiagnostic / NoDiagnostic assertions) +├── CodeFixTestFixture (abstract, TestCodeFix / NoCodeFix assertions) +├── CodeRefactoringTestFixture (abstract, TestCodeRefactoring assertions) +└── CompletionProviderFixture (abstract, completion assertions) +``` + +Each abstract fixture has a **Configurable** counterpart (`ConfigurableAnalyzerTestFixture`, etc.) that is `internal` and bridges a config object to the fixture's `protected virtual` properties. Users never instantiate these directly. + +### Factory entry point + +`RoslynFixtureFactory.Create()` is the only public API consumers use. It instantiates the analyzer/provider via `new T()`, wraps it in the matching Configurable fixture, and returns the abstract base type. All overloads accept an optional config object (`AnalyzerTestFixtureConfig`, `CodeFixTestFixtureConfig`, etc.) that inherits from `BaseTestFixtureConfig`. + +### Config hierarchy + +``` +BaseTestFixtureConfig (Language, References, AdditionalFiles, RuleSetPath, PackageCachePaths, CompilationOptions, ParseOptions, ProjectInfoCustomizer) +├── AnalyzerTestFixtureConfig +├── CodeFixTestFixtureConfig (+AdditionalAnalyzers) +├── CodeRefactoringTestFixtureConfig +└── CompletionProviderTestFixtureConfig +``` + +### Code markup for test spans + +Test input uses `[|` and `|]` markers to denote diagnostic/refactoring spans. `CodeMarkup` parses these, strips the markers from the code, and produces `IDiagnosticLocator` instances. Multiple markers per file are supported via `AllLocators`. Prefer `HasDiagnosticAtAllMarkers` / `NoDiagnosticAtAllMarkers` over single-marker methods. + +### Multi-document tests + +Use the `/*EOD*/` separator in a single code string to define multiple documents in one test project. The last segment becomes the "main" document. + +### NavCodeAnalysisBase + +An NUnit base class for test projects consuming this library. It detects the loaded `Microsoft.Dynamics.Nav.CodeAnalysis` assembly version at runtime and provides helpers (`RequireMinimumVersion`, `SkipTestIfVersionIsTooLow`, etc.) to conditionally skip tests based on AL DevTools version. + +## Conventions + +- **Nullable reference types** are enabled (`enable`). +- The single namespace is `RoslynTestKit` for all public types. Helper/utility types also live in `RoslynTestKit` or `RoslynTestKit.Utils` / `RoslynTestKit.CodeActionLocators`. +- `Configurable*` fixture classes are `internal`. Only the abstract base fixtures and `RoslynFixtureFactory` are public API. +- `ProjectSettings` is `internal` and carries config values from the fixture down to `AdhocWorkspace.AddProject`. +- Code comparison on failure uses DiffPlex for inline diffs and ApprovalTests for external diff tool launch (when a debugger is attached). +- Versioning uses [GitVersion](https://gitversion.net/) in Mainline mode. Version is derived from git history, not manually maintained. + +## CI/CD + +- **Pull requests**: build + pack + validate (via `dotnet-validate`). Skips draft PRs. +- **Main/master push**: build + pack + validate + create GitHub Release + publish to GitHub Packages + publish to NuGet.org. +- BC DevTools DLLs are downloaded in CI from the VS Code Marketplace via the custom `setup-bc-devtools` action. Two versions are fetched: `12.0.779795` (netstandard2.0) and `16.0.1463980` (net8.0).