Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7265686
fix: resolve CS8032 assembly version mismatch for System.Collections.…
rjmurillo Feb 16, 2026
368dc64
fix: add SRM check to MSBuild gate, fix PerfDiff NU1109
rjmurillo Feb 16, 2026
ffc4d3a
ci: add analyzer host compatibility check to CI pipeline
rjmurillo Feb 16, 2026
2c2ba70
fix(ci): correct DLL paths in analyzer host compatibility check
cursoragent Feb 16, 2026
94f0b9b
fix(build): enhance analyzer host compatibility validation
cursoragent Feb 16, 2026
3519f32
ci: add analyzer load integration test for supported SDK versions
rjmurillo Feb 16, 2026
e8414c5
ci: add net10.0 to analyzer load integration test matrix
rjmurillo Feb 16, 2026
9ff4f72
fix(ci): check build exit code in analyzer-load-test
cursoragent Feb 16, 2026
6fefa40
ci: add .NET Framework 4.7.2, 4.8, 4.8.1 to analyzer load test matrix
rjmurillo Feb 16, 2026
063d773
ci: use x64 runners for .NET Framework analyzer load tests
rjmurillo Feb 16, 2026
32d9a52
fix(ci): restore correct artifact paths for analyzer host validation
rjmurillo Feb 16, 2026
7a68343
ci: use msbuild.exe on Windows for .NET Framework analyzer load tests
rjmurillo Feb 16, 2026
7a9fd93
docs: add gh act workflow verification guide to CONTRIBUTING.md
rjmurillo Feb 16, 2026
67c7132
ci: test analyzer loading on both VS 2022 and VS 2026 hosts
rjmurillo Feb 16, 2026
c388191
ci: split build and test jobs, remove Windows ARM build
rjmurillo Feb 16, 2026
40802cc
ci: extract perf into separate job, unblock test and load-test
rjmurillo Feb 16, 2026
d4a3717
fix(ci): convert MSYS path to Windows format for local NuGet feed
cursoragent Feb 16, 2026
6bdd5db
ci: switch analyzer-load-test to pwsh, fix msbuild not found
rjmurillo Feb 16, 2026
86e37e7
ci: fix misleading OK output in host compatibility check
rjmurillo Feb 16, 2026
18a6d96
fix(ci): add null check and build exit code validation in analyzer-lo…
cursoragent Feb 16, 2026
49ad078
fix: address PR review feedback for analyzer load test robustness
rjmurillo Feb 16, 2026
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
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ updates:
- dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces"
- dependency-name: "Microsoft.CodeAnalysis.Common"
- dependency-name: "Microsoft.CodeAnalysis.Workspaces.Common"
# Analyzer-shipped BCL packages must match minimum supported SDK host.
# See: https://github.com/rjmurillo/moq.analyzers/issues/850
- dependency-name: "System.Collections.Immutable"
- dependency-name: "System.Reflection.Metadata"
Comment thread
rjmurillo marked this conversation as resolved.
150 changes: 150 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,36 @@ jobs:
- name: Setup, Restore, and Build Solution
uses: ./.github/actions/setup-restore-build

- name: Validate analyzer host compatibility
shell: pwsh
run: |
# Verify shipped analyzer DLLs don't reference assembly versions
# exceeding what the minimum supported SDK host (.NET 8) provides.
# See: https://github.com/rjmurillo/moq.analyzers/issues/850
$maxVersions = @{
'System.Collections.Immutable' = [Version]'8.0.0.0'
'System.Reflection.Metadata' = [Version]'8.0.0.0'
}
$shippedDlls = @(
'artifacts/bin/Moq.Analyzers/release/Moq.Analyzers.dll',
'artifacts/bin/Moq.Analyzers/release/Moq.CodeFixes.dll',
'artifacts/bin/Moq.Analyzers/release/Microsoft.CodeAnalysis.AnalyzerUtilities.dll'
)
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
rjmurillo marked this conversation as resolved.
$failed = $false
foreach ($dll in $shippedDlls) {
$name = [System.IO.Path]::GetFileName($dll)
$bytes = [System.IO.File]::ReadAllBytes($dll)
$asm = [System.Reflection.Assembly]::Load($bytes)
foreach ($ref in $asm.GetReferencedAssemblies()) {
if ($maxVersions.ContainsKey($ref.Name) -and $ref.Version -gt $maxVersions[$ref.Name]) {
Write-Error "$name references $($ref.Name) $($ref.Version), max allowed is $($maxVersions[$ref.Name])"
$failed = $true
}
}
Write-Host "$name - OK"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if ($failed) { exit 1 }

- name: Restore Code Coverage history
uses: actions/download-artifact@v7
with:
Expand Down Expand Up @@ -225,3 +255,123 @@ jobs:
name: artifacts-${{ matrix.os }}
path: ./artifacts
if-no-files-found: error

# Verify the shipped nupkg loads without CS8032 on every supported SDK and TFM.
# This is the end-to-end integration test for issue #850.
# The analyzer targets netstandard2.0 so it must load for both .NET and
# .NET Framework projects. The compiler host is the .NET SDK regardless of
# the target TFM, but we test all TFMs to catch packaging issues too.
analyzer-load-test:
needs: build
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
include:
# .NET (modern) TFMs on ARM runners
- dotnet-version: '8.0.x'
tfm: net8.0
runs-on: ubuntu-24.04-arm
- dotnet-version: '9.0.x'
tfm: net9.0
runs-on: ubuntu-24.04-arm
- dotnet-version: '10.0.x'
tfm: net10.0
runs-on: ubuntu-24.04-arm
# .NET Framework TFMs require x64 runners
- dotnet-version: '8.0.x'
tfm: net472
runs-on: ubuntu-24.04
- dotnet-version: '8.0.x'
tfm: net48
runs-on: ubuntu-24.04
- dotnet-version: '8.0.x'
tfm: net481
runs-on: ubuntu-24.04

steps:
- name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ matrix.dotnet-version }}

- name: Download nupkg artifact
uses: actions/download-artifact@v7
with:
name: packages-ubuntu-24.04-arm
path: ./local-feed

- name: Build test project and verify analyzer loads
shell: bash
run: |
# Find the nupkg and extract its version
PKG=$(find ./local-feed -name 'Moq.Analyzers.*.nupkg' ! -name '*.symbols.*' | head -1)
VERSION=$(basename "$PKG" | sed 's/Moq\.Analyzers\.\(.*\)\.nupkg/\1/')
FEED_DIR=$(cd "$(dirname "$PKG")" && pwd)
echo "Testing Moq.Analyzers $VERSION on .NET ${{ matrix.dotnet-version }} (${{ matrix.tfm }})"

mkdir test-project && cd test-project

# Point NuGet at the local feed + nuget.org for framework deps
cat > nuget.config <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local" value="$FEED_DIR" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
Comment thread
cursor[bot] marked this conversation as resolved.
</packageSources>
</configuration>
EOF

# Minimal project referencing the analyzer nupkg.
# Microsoft.NETFramework.ReferenceAssemblies provides targeting packs
# for .NET Framework TFMs on Linux (no-op for .NET Core TFMs).
cat > TestAnalyzerLoad.csproj <<EOF
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>${{ matrix.tfm }}</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq.Analyzers" Version="$VERSION" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
</Project>
EOF

# Minimal source file; the compiler loads analyzers regardless of content
cat > Placeholder.cs <<'EOF'
namespace TestAnalyzerLoad;
public class Placeholder { }
EOF

# Build with normal verbosity so analyzer warnings are visible
set +e
BUILD_OUTPUT=$(dotnet build -v n 2>&1)
BUILD_EXIT=$?
set -e
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

echo "$BUILD_OUTPUT"

if echo "$BUILD_OUTPUT" | grep -q "CS8032"; then
echo ""
echo "::error::CS8032: analyzer failed to load on .NET ${{ matrix.dotnet-version }} (${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
exit 1
fi

if echo "$BUILD_OUTPUT" | grep -qi "could not load file or assembly"; then
echo ""
echo "::error::Assembly binding failure on .NET ${{ matrix.dotnet-version }} (${{ matrix.tfm }}). See https://github.com/rjmurillo/moq.analyzers/issues/850"
exit 1
fi

if [ "$BUILD_EXIT" -ne 0 ]; then
echo ""
echo "::error::Build failed with exit code $BUILD_EXIT on .NET ${{ matrix.dotnet-version }}. See build output above for details."
exit 1
fi

echo ""
echo "Analyzer loaded successfully on .NET ${{ matrix.dotnet-version }} (${{ matrix.tfm }})"
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
<Import Project="build/targets/versioning/Versioning.targets" />
<Import Project="build/targets/tests/Tests.targets" />
<Import Project="build/targets/codeanalysis/CodeAnalysis.targets" />
<Import Project="build/targets/packaging/Packaging.targets" />
</Project>
20 changes: 18 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.AnalyzerUtilities" Version="4.14.0" />
<!--
AnalyzerUtilities is bundled in the shipped analyzer nupkg.
Version 4.14.0+ references System.Collections.Immutable 9.0.0.0,
which fails to load in .NET 8 SDK hosts (CS8032). Use 3.3.4 which
has no SCI dependency.
Comment thread
rjmurillo marked this conversation as resolved.
Outdated
See: https://github.com/rjmurillo/moq.analyzers/issues/850
-->
<PackageVersion Include="Microsoft.CodeAnalysis.AnalyzerUtilities" Version="3.3.4" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="GetPackFromProject" Version="1.0.10" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
Expand All @@ -34,7 +41,16 @@
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.28" />
</ItemGroup>
<ItemGroup Label="Transitive pins">
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<!--
CAUTION: Packages pinned here flow into the shipped analyzer DLL.
The analyzer runs in the USER's compiler host, not ours.
Versions must not exceed what the minimum supported SDK host provides.
Currently: .NET 8 SDK (assembly version 8.0.0.0).

Build target ValidateAnalyzerHostCompatibility enforces this constraint.
See: https://github.com/rjmurillo/moq.analyzers/issues/850
-->
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.Formats.Asn1" Version="10.0.0" />
</ItemGroup>
</Project>
40 changes: 40 additions & 0 deletions build/targets/packaging/Packaging.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project>
<!--
Analyzer Host Compatibility Validation

Analyzers run inside the user's compiler host (csc). The host provides
BCL assemblies at specific versions. If the analyzer was compiled against
a NEWER version than the host provides, assembly binding fails with CS8032.

This target enforces version ceilings on host-provided packages for all
shipped analyzer projects. It is a build-breaking gate.

See: https://github.com/rjmurillo/moq.analyzers/issues/850
-->
<PropertyGroup>
<!-- Maximum assembly versions the minimum supported host (.NET 8 SDK) provides -->
<_MaxSystemCollectionsImmutableMajor>8</_MaxSystemCollectionsImmutableMajor>
<_MaxSystemReflectionMetadataMajor>8</_MaxSystemReflectionMetadataMajor>
</PropertyGroup>

<Target Name="ValidateAnalyzerHostCompatibility"
AfterTargets="ResolvePackageAssets"
Condition="'$(GeneratePackageOnBuild)' == 'true' OR '$(MSBuildProjectName)' == 'Moq.CodeFixes'">
Comment thread
rjmurillo marked this conversation as resolved.

<ItemGroup>
<_SciRef Include="@(ResolvedCompileFileDefinitions)"
Condition="'%(NuGetPackageId)' == 'System.Collections.Immutable'" />
<_SrmRef Include="@(ResolvedCompileFileDefinitions)"
Condition="'%(NuGetPackageId)' == 'System.Reflection.Metadata'" />
</ItemGroup>

<Error Condition="'@(_SciRef)' != '' AND
$([System.Version]::Parse('%(_SciRef.NuGetPackageVersion)').Major) &gt; $(_MaxSystemCollectionsImmutableMajor)"
Text="BUILD BLOCKED: System.Collections.Immutable resolved to %(_SciRef.NuGetPackageVersion) for shipped analyzer project '$(MSBuildProjectName)'. Maximum allowed major version is $(_MaxSystemCollectionsImmutableMajor).x. The analyzer runs in the user's compiler host which may only have version $(_MaxSystemCollectionsImmutableMajor).0.0.0. See: https://github.com/rjmurillo/moq.analyzers/issues/850" />

<Error Condition="'@(_SrmRef)' != '' AND
$([System.Version]::Parse('%(_SrmRef.NuGetPackageVersion)').Major) &gt; $(_MaxSystemReflectionMetadataMajor)"
Text="BUILD BLOCKED: System.Reflection.Metadata resolved to %(_SrmRef.NuGetPackageVersion) for shipped analyzer project '$(MSBuildProjectName)'. Maximum allowed major version is $(_MaxSystemReflectionMetadataMajor).x. The analyzer runs in the user's compiler host which may only have version $(_MaxSystemReflectionMetadataMajor).0.0.0. See: https://github.com/rjmurillo/moq.analyzers/issues/850" />

</Target>
</Project>
Comment thread
rjmurillo marked this conversation as resolved.
10 changes: 10 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"description": "Analyzer-shipped BCL packages must match minimum supported SDK host. Manual review required.",
"matchPackageNames": [
"System.Collections.Immutable",
"System.Reflection.Metadata",
"Microsoft.CodeAnalysis.AnalyzerUtilities"
],
"automerge": false,
"labels": ["analyzer-compat"]
}
],
"platformAutomerge": true
Expand Down
2 changes: 2 additions & 0 deletions src/tools/PerfDiff/PerfDiff.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.CommandLine.Rendering" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" />
<!-- PerfDiff is not shipped. Allow higher SCI for TraceEvent dependency. -->
<PackageReference Include="System.Collections.Immutable" VersionOverride="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="BenchmarkDotNet" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.AnalyzerUtilities" />
<!-- Benchmarks are not shipped. Allow higher SCI for TraceEvent dependency. -->
<PackageReference Include="System.Collections.Immutable" VersionOverride="10.0.0" />
<ProjectReference Include="$(RepoRoot)/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj" />
</ItemGroup>

Expand Down
101 changes: 101 additions & 0 deletions tests/Moq.Analyzers.Test/AnalyzerAssemblyCompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Reflection;
using System.Runtime.Loader;

namespace Moq.Analyzers.Test;

public class AnalyzerAssemblyCompatibilityTests
{
// Primary shipped assemblies built by this project
private static readonly string PrimaryAnalyzerAssembly = "Moq.Analyzers";
private static readonly string PrimaryCodeFixAssembly = "Moq.CodeFixes";

// Bundled third-party assemblies included in the package
private static readonly string BundledAnalyzerUtilities = "Microsoft.CodeAnalysis.AnalyzerUtilities";

// Maximum assembly versions that the minimum supported SDK host (.NET 8) provides.
// The analyzer must not reference anything higher, or it will fail to load with CS8032.
// See: https://github.com/rjmurillo/moq.analyzers/issues/850
private static readonly Version MaxImmutableVersion = new(8, 0, 0, 0);
private static readonly Version MaxMetadataVersion = new(8, 0, 0, 0);

public static TheoryData<string> ShippedAssemblies =>
new()
{
{ PrimaryAnalyzerAssembly },
{ PrimaryCodeFixAssembly },
{ BundledAnalyzerUtilities },
};

[Theory]
[MemberData(nameof(ShippedAssemblies))]
public void ShippedDlls_MustNotExceedMinimumHostAssemblyVersions(string assemblyName)
{
FileInfo testAssembly = new(Assembly.GetExecutingAssembly().Location);
FileInfo dllFile = new(Path.Combine(testAssembly.DirectoryName!, $"{assemblyName}.dll"));

Assert.True(dllFile.Exists, $"Expected shipped DLL not found: {dllFile.FullName}");
Comment thread
cursor[bot] marked this conversation as resolved.

AssemblyLoadContext context = new("compat-check", isCollectible: true);
try
{
Assembly assembly = context.LoadFromAssemblyPath(dllFile.FullName);

// For bundled third-party DLLs, verify we are testing the same artifact
// that the primary analyzer assembly references, not a different version
// that might exist in the test output from the test project's own dependencies.
if (string.Equals(assemblyName, BundledAnalyzerUtilities, StringComparison.Ordinal))
{
VerifyBundledAssemblyMatchesPrimaryReference(context, testAssembly.DirectoryName!, assembly);
}

AssemblyName[] references = assembly.GetReferencedAssemblies();

AssertVersionNotExceeded(assemblyName, references, "System.Collections.Immutable", MaxImmutableVersion);
AssertVersionNotExceeded(assemblyName, references, "System.Reflection.Metadata", MaxMetadataVersion);
}
finally
{
context.Unload();
}
}

private static void VerifyBundledAssemblyMatchesPrimaryReference(
AssemblyLoadContext context,
string outputDirectory,
Assembly bundledAssembly)
{
// Load the primary analyzer assembly to verify the bundled assembly matches
// what the analyzer actually references. This ensures we're testing the
// artifact that will be packaged, not a different version from the test project.
string primaryPath = Path.Combine(outputDirectory, $"{PrimaryAnalyzerAssembly}.dll");
Assembly primaryAssembly = context.LoadFromAssemblyPath(primaryPath);

AssemblyName? expectedReference = primaryAssembly
.GetReferencedAssemblies()
.FirstOrDefault(r => string.Equals(r.Name, bundledAssembly.GetName().Name, StringComparison.Ordinal));

Assert.NotNull(expectedReference);
Assert.Equal(
expectedReference.Version,
bundledAssembly.GetName().Version);
Comment thread
rjmurillo marked this conversation as resolved.
}

private static void AssertVersionNotExceeded(
string assemblyName,
AssemblyName[] references,
string referenceName,
Version maxVersion)
{
AssemblyName? reference = references.FirstOrDefault(
r => string.Equals(r.Name, referenceName, StringComparison.Ordinal));

if (reference?.Version is null)
{
return;
}

Assert.True(
reference.Version <= maxVersion,
$"{assemblyName} references {referenceName} version {reference.Version}, but the minimum supported SDK host only provides {maxVersion}. See: https://github.com/rjmurillo/moq.analyzers/issues/850");
}
}
Loading