|
| 1 | +using System.Collections.Generic; |
| 2 | +using System.IO; |
| 3 | +using System.Reflection; |
| 4 | +using System.Reflection.Metadata; |
| 5 | +using System.Reflection.PortableExecutable; |
| 6 | + |
| 7 | +namespace TUnit.Tests.Shared; |
| 8 | + |
| 9 | +/// <summary> |
| 10 | +/// Verifies that a code fixer assembly carries no IL reference to its analyzer project's |
| 11 | +/// <c>Rules</c> type. Guards against https://github.com/thomhurst/TUnit/issues/6157. |
| 12 | +/// </summary> |
| 13 | +/// <remarks> |
| 14 | +/// Code fixer assemblies ship in the version-agnostic <c>analyzers/dotnet/cs</c> folder while the |
| 15 | +/// analyzer assemblies ship per-Roslyn (<c>analyzers/dotnet/roslyn4.x/cs</c>), and the dependency |
| 16 | +/// resolves at runtime by simple name. Visual Studio cannot unload analyzer assemblies, so after a |
| 17 | +/// package update (or with mixed TUnit versions in one VS session) a new code fixer can bind |
| 18 | +/// against a stale analyzer assembly. Any IL reference to the <c>Rules</c> type — e.g. |
| 19 | +/// <c>Rules.X.Id</c> inside the eagerly-evaluated <c>FixableDiagnosticIds</c> — then throws |
| 20 | +/// <see cref="System.MissingFieldException"/> for rules the stale assembly doesn't have. Code |
| 21 | +/// fixers must use the compile-time-baked <c>DiagnosticIds</c> constants instead, which this |
| 22 | +/// helper enforces at the IL level: a <c>TypeReference</c> to <c>Rules</c> appears for any usage |
| 23 | +/// (field access, <c>typeof</c>, method call), so an empty result proves full decoupling. |
| 24 | +/// <c>DiagnosticIds</c> itself is also scanned — its members must stay <c>const</c>; changing one |
| 25 | +/// to <c>static readonly</c> would silently reintroduce a runtime type reference, which surfaces |
| 26 | +/// here as a <c>TypeReference</c> to <c>DiagnosticIds</c>. |
| 27 | +/// <para> |
| 28 | +/// Linked into each code fixer test project via |
| 29 | +/// <c><Compile Include="..\SharedTestHelpers\RulesDecouplingVerifier.cs"></c>. |
| 30 | +/// </para> |
| 31 | +/// </remarks> |
| 32 | +internal static class RulesDecouplingVerifier |
| 33 | +{ |
| 34 | + /// <summary> |
| 35 | + /// Returns the fully-qualified names of all <c>Rules</c> or <c>DiagnosticIds</c> type |
| 36 | + /// references in <paramref name="codeFixersAssembly"/> whose namespace is |
| 37 | + /// <paramref name="rulesNamespace"/>. An empty list means the assembly is fully decoupled. |
| 38 | + /// </summary> |
| 39 | + public static List<string> FindRulesTypeReferences(Assembly codeFixersAssembly, string rulesNamespace) |
| 40 | + { |
| 41 | + using var stream = File.OpenRead(codeFixersAssembly.Location); |
| 42 | + using var peReader = new PEReader(stream); |
| 43 | + var metadata = peReader.GetMetadataReader(); |
| 44 | + |
| 45 | + var rulesReferences = new List<string>(); |
| 46 | + |
| 47 | + foreach (var handle in metadata.TypeReferences) |
| 48 | + { |
| 49 | + var typeReference = metadata.GetTypeReference(handle); |
| 50 | + var name = metadata.GetString(typeReference.Name); |
| 51 | + var typeNamespace = metadata.GetString(typeReference.Namespace); |
| 52 | + |
| 53 | + if (name is "Rules" or "DiagnosticIds" && typeNamespace == rulesNamespace) |
| 54 | + { |
| 55 | + rulesReferences.Add($"{typeNamespace}.{name}"); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + return rulesReferences; |
| 60 | + } |
| 61 | +} |
0 commit comments