Skip to content

Commit 44e6f29

Browse files
mcpolo99RandomCrocodile
andauthored
feature: Tier 3 custom Roslyn analyzers CX001–CX004 (#49, #46) (#95)
* feature: add Confuser.Analyzers project with CX004 analyzer (#49) First increment of the Tier 3 custom analyzers (#49, parent #46). Establishes the Confuser.Analyzers project (netstandard2.0 Roslyn analyzer) and wires it into every project via ConfuserEx.Common.targets as an analyzer reference. CX004 — flags Assembly.GetTypes()/Module.GetTypes() that are not guarded against ReflectionTypeLoadException (the cause of packer/plugin startup crashes). The analyzer resolves the invocation symbol and only fires for System.Reflection types, so dnlib's ModuleDef.GetTypes() (which returns TypeDef and never throws) is ignored. - Fixed the two real CX004 violations: PluginDiscovery.AddPlugins and ComponentDiscovery.LoadComponents now catch ReflectionTypeLoadException and keep the types that loaded. - Analyzer wired with SkipGetTargetFrameworkProperties + UndefineProperties so the netstandard2.0 analyzer attaches to all targets including net20. - 4 analyzer unit tests (fires on unguarded; silent when guarded by RTLE/Exception or on unrelated GetTypes()). Full solution builds clean with zero CX004 warnings. Remaining under #49 (follow-up): CX002 (unguarded Resolve*Def), CX003 (ResolveThrow audit). CX001's premise is stale — the listed call sites were refactored to a context-aware Import helper; the only host-reflection path left is that helper's fallback (Confuser.Protections/Utils.cs). * feature: add CX003 Resolve...Throw audit analyzer (#49) CX003 surfaces every call to a dnlib Resolve...Throw helper (ResolveThrow, ResolveTypeDefThrow, ResolveMethodDefThrow, ResolveFieldThrow) at Info severity — an awareness rule for spots that crash on unresolvable references. Info severity means no build noise (the ~34 existing intentional uses are not reported as warnings). 5 tests. * feature: add CX002 and CX001 analyzers, completing Tier 3 (#49) Completes the four Tier 3 analyzers. CX002 — flags a non-throwing ResolveTypeDef()/ResolveMethodDef() result that is dereferenced immediately with no null check (crashes on external/unresolvable references). Only genuine dereferences are flagged (x.ResolveTypeDef().Member / [i]); null-conditional access, assignment, return, and passing the result as an argument (e.g. to dnlib's null-tolerant SigComparer.Equals, as VTableAnalyzer does) are correctly not reported. No existing violations — pure prevention. CX001 — redesigned against the current code. The issue's original call sites were already refactored to a context-aware Import(context, Type, method) helper; the only remaining host-reflection path is that helper's last-resort fallback. CX001 now flags Import(...) whose argument is Type.GetMethod/GetConstructor/GetField/ GetProperty (host reflection → wrong-corlib reference). The single intentional fallback in Confuser.Protections/Utils.cs is isolated and suppressed with a documented pragma; new occurrences are flagged. 8 new analyzer tests (17 total). Full solution builds clean — zero CX diagnostics. --------- Co-authored-by: RandomCrocodile <mawi@polosab.com>
1 parent a8ad297 commit 44e6f29

16 files changed

Lines changed: 674 additions & 6 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<!-- Roslyn analyzer assembly. Deliberately standalone (does NOT import
4+
ConfuserEx.Common.props) — analyzers must be a single netstandard2.0 assembly
5+
with no runtime output and no strong-name/multi-target concerns. -->
6+
<PropertyGroup>
7+
<TargetFramework>netstandard2.0</TargetFramework>
8+
<IncludeBuildOutput>false</IncludeBuildOutput>
9+
<Nullable>enable</Nullable>
10+
<LangVersion>latest</LangVersion>
11+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
12+
<IsPackable>false</IsPackable>
13+
<!-- These analyzers ship inside this repo only; they are never consumed as a NuGet package. -->
14+
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
15+
<!-- RS2008 (analyzer release tracking) only matters for analyzers shipped as packages. -->
16+
<NoWarn>$(NoWarn);RS2008</NoWarn>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all">
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Confuser.Analyzers {
2+
/// <summary>
3+
/// Diagnostic IDs for the ConfuserEx-specific analyzers. Each catches a bug class that has
4+
/// actually been fixed in the codebase, to prevent regressions at compile time.
5+
/// </summary>
6+
internal static class DiagnosticIds {
7+
/// <summary>Imports a member from the host runtime instead of the target's corlib.</summary>
8+
public const string HostRuntimeImport = "CX001";
9+
10+
/// <summary><c>ResolveTypeDef()</c>/<c>ResolveMethodDef()</c> used without a null check.</summary>
11+
public const string UnguardedResolve = "CX002";
12+
13+
/// <summary>Usage of a <c>...Throw</c> resolve helper (audit / awareness).</summary>
14+
public const string ResolveThrowAudit = "CX003";
15+
16+
/// <summary><c>GetTypes()</c> without handling <c>ReflectionTypeLoadException</c>.</summary>
17+
public const string UnhandledReflectionTypeLoad = "CX004";
18+
19+
public const string Category = "ConfuserEx";
20+
}
21+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Confuser.Analyzers {
8+
/// <summary>
9+
/// CX001 — flags <c>Import(...)</c> whose argument is a member resolved via reflection on
10+
/// the host runtime (<c>someType.GetMethod(...)</c>, <c>GetConstructor</c>, <c>GetField</c>,
11+
/// <c>GetProperty</c> where the receiver is a <see cref="System.Type" />). Importing a
12+
/// host-runtime member into a target module produces a reference to the wrong corlib
13+
/// (e.g. the obfuscator's .NET rather than the target's), which breaks the protected
14+
/// assembly. Resolve the member through the target module's corlib instead.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class HostRuntimeImportAnalyzer : DiagnosticAnalyzer {
18+
static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
19+
DiagnosticIds.HostRuntimeImport,
20+
"Importing a member resolved via host reflection",
21+
"Import() argument is resolved with Type.{0}() on the host runtime; resolve the member through the target module's corlib to avoid a wrong-corlib reference",
22+
DiagnosticIds.Category,
23+
DiagnosticSeverity.Warning,
24+
isEnabledByDefault: true,
25+
description: "Importing a System.Reflection member (from Type.GetMethod/GetConstructor/GetField/" +
26+
"GetProperty) into a dnlib module references the obfuscator's runtime corlib rather than the " +
27+
"target's, producing a broken assembly. Resolve the member through the target module's corlib.");
28+
29+
static readonly ImmutableHashSet<string> ReflectionGetters =
30+
ImmutableHashSet.Create("GetMethod", "GetConstructor", "GetField", "GetProperty");
31+
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
33+
34+
public override void Initialize(AnalysisContext context) {
35+
context.EnableConcurrentExecution();
36+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
37+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
38+
}
39+
40+
static void Analyze(SyntaxNodeAnalysisContext context) {
41+
var invocation = (InvocationExpressionSyntax)context.Node;
42+
if (invocation.Expression is not MemberAccessExpressionSyntax member ||
43+
member.Name.Identifier.ValueText != "Import")
44+
return;
45+
46+
foreach (var argument in invocation.ArgumentList.Arguments) {
47+
if (argument.Expression is not InvocationExpressionSyntax inner ||
48+
inner.Expression is not MemberAccessExpressionSyntax innerMember)
49+
continue;
50+
51+
var getterName = innerMember.Name.Identifier.ValueText;
52+
if (!ReflectionGetters.Contains(getterName))
53+
continue;
54+
55+
if (context.SemanticModel.GetSymbolInfo(inner, context.CancellationToken).Symbol is not IMethodSymbol getter ||
56+
getter.ContainingType?.ToDisplayString() != "System.Type")
57+
continue;
58+
59+
context.ReportDiagnostic(Diagnostic.Create(Rule, innerMember.Name.GetLocation(), getterName));
60+
return;
61+
}
62+
}
63+
}
64+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Confuser.Analyzers {
8+
/// <summary>
9+
/// CX003 — an awareness/audit rule that surfaces every call to a dnlib <c>Resolve…Throw</c>
10+
/// helper (<c>ResolveThrow</c>, <c>ResolveTypeDefThrow</c>, <c>ResolveMethodDefThrow</c>,
11+
/// <c>ResolveFieldThrow</c>). These throw when a reference cannot be resolved, which crashes
12+
/// obfuscation on assemblies with external/unresolvable members. Most uses are intentional;
13+
/// this reports at <see cref="DiagnosticSeverity.Info" /> so the spots can be evaluated
14+
/// without adding build noise.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class ResolveThrowAuditAnalyzer : DiagnosticAnalyzer {
18+
static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
19+
DiagnosticIds.ResolveThrowAudit,
20+
"Throwing resolve helper used",
21+
"'{0}' throws when resolution fails; confirm the target is always resolvable or prefer the non-throwing overload with a null check",
22+
DiagnosticIds.Category,
23+
DiagnosticSeverity.Info,
24+
isEnabledByDefault: true,
25+
description: "dnlib's Resolve...Throw helpers throw when a type/member reference cannot be " +
26+
"resolved. This surfaces each usage for review; external or unresolvable references should " +
27+
"use the non-throwing Resolve... overload with a null check instead.");
28+
29+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
30+
31+
public override void Initialize(AnalysisContext context) {
32+
context.EnableConcurrentExecution();
33+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
34+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
35+
}
36+
37+
static void Analyze(SyntaxNodeAnalysisContext context) {
38+
var invocation = (InvocationExpressionSyntax)context.Node;
39+
if (invocation.Expression is not MemberAccessExpressionSyntax member)
40+
return;
41+
42+
var name = member.Name.Identifier.ValueText;
43+
if (!IsResolveThrowName(name))
44+
return;
45+
46+
context.ReportDiagnostic(Diagnostic.Create(Rule, member.Name.GetLocation(), name));
47+
}
48+
49+
static bool IsResolveThrowName(string name) =>
50+
name.Length > "ResolveThrow".Length - 1 &&
51+
name.StartsWith("Resolve", System.StringComparison.Ordinal) &&
52+
name.EndsWith("Throw", System.StringComparison.Ordinal);
53+
}
54+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Confuser.Analyzers {
8+
/// <summary>
9+
/// CX002 — flags the result of a non-throwing dnlib <c>ResolveTypeDef()</c> /
10+
/// <c>ResolveMethodDef()</c> being dereferenced immediately, with no null check. Those
11+
/// methods return <c>null</c> for external or unresolvable references, so a direct
12+
/// dereference crashes with a <see cref="System.NullReferenceException" /> on assemblies
13+
/// that reference types outside the obfuscation set.
14+
/// </summary>
15+
/// <remarks>
16+
/// Only a genuine dereference of the result is flagged — <c>x.ResolveTypeDef().Member</c> or
17+
/// <c>x.ResolveMethodDef()[i]</c>. A null-conditional (<c>?.</c>), an assignment, a
18+
/// <c>return</c>, or passing the result as an argument (e.g. to dnlib's null-tolerant
19+
/// <c>SigComparer.Equals</c>) is not a crash and is intentionally not reported.
20+
/// </remarks>
21+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
22+
public sealed class UnguardedResolveAnalyzer : DiagnosticAnalyzer {
23+
static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
24+
DiagnosticIds.UnguardedResolve,
25+
"Resolve result dereferenced without a null check",
26+
"'{0}()' returns null for unresolvable references; check the result for null before dereferencing it",
27+
DiagnosticIds.Category,
28+
DiagnosticSeverity.Warning,
29+
isEnabledByDefault: true,
30+
description: "dnlib's ResolveTypeDef()/ResolveMethodDef() return null when a reference cannot be " +
31+
"resolved (external or unresolvable types). Dereferencing the result directly crashes obfuscation " +
32+
"on such assemblies; guard it with a null check or the null-conditional operator.");
33+
34+
static readonly ImmutableHashSet<string> ResolveMethods =
35+
ImmutableHashSet.Create("ResolveTypeDef", "ResolveMethodDef");
36+
37+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
38+
39+
public override void Initialize(AnalysisContext context) {
40+
context.EnableConcurrentExecution();
41+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
42+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
43+
}
44+
45+
static void Analyze(SyntaxNodeAnalysisContext context) {
46+
var invocation = (InvocationExpressionSyntax)context.Node;
47+
if (invocation.Expression is not MemberAccessExpressionSyntax member ||
48+
!ResolveMethods.Contains(member.Name.Identifier.ValueText) ||
49+
invocation.ArgumentList.Arguments.Count != 0)
50+
return;
51+
52+
if (!IsImmediatelyDereferenced(invocation))
53+
return;
54+
55+
context.ReportDiagnostic(Diagnostic.Create(Rule, member.Name.GetLocation(),
56+
member.Name.Identifier.ValueText));
57+
}
58+
59+
/// <summary>
60+
/// Returns <c>true</c> only when <paramref name="invocation" /> is the receiver of a
61+
/// plain member access or element access — i.e. its result is dereferenced right away
62+
/// without a null guard. A null-conditional access makes the parent a
63+
/// <see cref="ConditionalAccessExpressionSyntax" />, which is safe and returns false.
64+
/// </summary>
65+
static bool IsImmediatelyDereferenced(InvocationExpressionSyntax invocation) {
66+
switch (invocation.Parent) {
67+
case MemberAccessExpressionSyntax parentMember when parentMember.Expression == invocation:
68+
return true;
69+
case ElementAccessExpressionSyntax parentElement when parentElement.Expression == invocation:
70+
return true;
71+
default:
72+
return false;
73+
}
74+
}
75+
}
76+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Confuser.Analyzers {
8+
/// <summary>
9+
/// CX004 — flags <c>Assembly.GetTypes()</c> / <c>Module.GetTypes()</c> that are not guarded
10+
/// against <see cref="System.Reflection.ReflectionTypeLoadException" />. That exception is
11+
/// thrown whenever a contained type cannot be loaded (common for plugin assemblies with
12+
/// unresolved dependencies) and caused packer/plugin startup crashes.
13+
/// </summary>
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public sealed class UnhandledReflectionTypeLoadAnalyzer : DiagnosticAnalyzer {
16+
static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
17+
DiagnosticIds.UnhandledReflectionTypeLoad,
18+
"GetTypes() without ReflectionTypeLoadException handling",
19+
"'{0}.GetTypes()' can throw ReflectionTypeLoadException when a contained type is unresolvable; wrap it in a try/catch",
20+
DiagnosticIds.Category,
21+
DiagnosticSeverity.Warning,
22+
isEnabledByDefault: true,
23+
description: "Assembly.GetTypes() and Module.GetTypes() throw ReflectionTypeLoadException when any " +
24+
"contained type cannot be loaded. Guard the call (and prefer ex.Types on the exception) to avoid " +
25+
"startup crashes when loading plugin or external assemblies.");
26+
27+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
28+
29+
public override void Initialize(AnalysisContext context) {
30+
context.EnableConcurrentExecution();
31+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
32+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
33+
}
34+
35+
static void Analyze(SyntaxNodeAnalysisContext context) {
36+
var invocation = (InvocationExpressionSyntax)context.Node;
37+
if (invocation.Expression is not MemberAccessExpressionSyntax member ||
38+
member.Name.Identifier.ValueText != "GetTypes")
39+
return;
40+
41+
if (context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is not IMethodSymbol method ||
42+
method.Name != "GetTypes" || !method.Parameters.IsEmpty)
43+
return;
44+
45+
var containingType = method.ContainingType?.ToDisplayString();
46+
if (containingType != "System.Reflection.Assembly" && containingType != "System.Reflection.Module")
47+
return;
48+
49+
var rtle = context.Compilation.GetTypeByMetadataName("System.Reflection.ReflectionTypeLoadException");
50+
if (IsGuarded(invocation, context.SemanticModel, rtle, context.CancellationToken))
51+
return;
52+
53+
context.ReportDiagnostic(Diagnostic.Create(Rule, member.Name.GetLocation(), containingType));
54+
}
55+
56+
static bool IsGuarded(SyntaxNode node, SemanticModel model, INamedTypeSymbol? rtle, System.Threading.CancellationToken ct) {
57+
for (var current = node.Parent; current != null; current = current.Parent) {
58+
if (current is TryStatementSyntax tryStmt &&
59+
tryStmt.Block.Span.Contains(node.Span) &&
60+
CatchesReflectionTypeLoad(tryStmt, model, rtle, ct))
61+
return true;
62+
63+
// Do not walk past the enclosing method / lambda / local-function boundary.
64+
if (current is BaseMethodDeclarationSyntax ||
65+
current is AnonymousFunctionExpressionSyntax ||
66+
current is LocalFunctionStatementSyntax)
67+
break;
68+
}
69+
70+
return false;
71+
}
72+
73+
static bool CatchesReflectionTypeLoad(TryStatementSyntax tryStmt, SemanticModel model, INamedTypeSymbol? rtle,
74+
System.Threading.CancellationToken ct) {
75+
foreach (var clause in tryStmt.Catches) {
76+
// A general 'catch { }' (no declared type) catches everything.
77+
if (clause.Declaration is null)
78+
return true;
79+
80+
var caught = model.GetTypeInfo(clause.Declaration.Type, ct).Type as INamedTypeSymbol;
81+
if (caught is null)
82+
continue;
83+
84+
// The catch guards the call if ReflectionTypeLoadException is assignable to the caught
85+
// type (i.e. the caught type is RTLE or one of its base types such as SystemException /
86+
// Exception). If we cannot resolve RTLE, fall back to name matching on the base chain.
87+
if (rtle is not null) {
88+
for (var t = rtle; t is not null; t = t.BaseType) {
89+
if (SymbolEqualityComparer.Default.Equals(t, caught))
90+
return true;
91+
}
92+
}
93+
else {
94+
var name = caught.ToDisplayString();
95+
if (name == "System.Exception" || name == "System.SystemException" ||
96+
name == "System.Reflection.ReflectionTypeLoadException")
97+
return true;
98+
}
99+
}
100+
101+
return false;
102+
}
103+
}
104+
}

Confuser.Core/PluginDiscovery.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,18 @@ public static bool HasAccessibleDefConstructor(Type type) {
5555
protected static void AddPlugins(
5656
ConfuserContext context, IList<Protection> protections, IList<Packer> packers,
5757
IList<ConfuserComponent> components, Assembly asm) {
58-
foreach (var module in asm.GetLoadedModules())
59-
foreach (var i in module.GetTypes()) {
58+
foreach (var module in asm.GetLoadedModules()) {
59+
Type[] moduleTypes;
60+
try {
61+
moduleTypes = module.GetTypes();
62+
}
63+
catch (ReflectionTypeLoadException ex) {
64+
// A plugin assembly may reference dependencies that are not present; keep the
65+
// types that did load instead of crashing plugin discovery.
66+
moduleTypes = Array.FindAll(ex.Types, t => t != null);
67+
}
68+
69+
foreach (var i in moduleTypes) {
6070
if (i.IsAbstract || !HasAccessibleDefConstructor(i))
6171
continue;
6272

@@ -85,6 +95,7 @@ protected static void AddPlugins(
8595
}
8696
}
8797
}
98+
}
8899
context.CheckCancellation();
89100
}
90101

Confuser.Protections/Utils.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ internal static class Utils {
1212
public static IMethod Import(this ModuleDef module, ConfuserContext context, Type classType, string method) {
1313
var corLib = context.Resolver.Resolve(context.CurrentModule?.CorLibTypes.AssemblyRef, context.CurrentModule);
1414
var typeInfo = corLib?.ManifestModule.Find(classType.FullName, true);
15-
return (typeInfo == null) ? module.Import(classType.GetMethod(method)) : module.Import(typeInfo.FindMethod(method));
15+
if (typeInfo != null)
16+
return module.Import(typeInfo.FindMethod(method));
17+
18+
// CX001: intentional last-resort fallback. When the target module's corlib type cannot be
19+
// resolved through the context (rare), host reflection is the only available source for the
20+
// member reference. New code must resolve through the context instead of copying this.
21+
#pragma warning disable CX001
22+
return module.Import(classType.GetMethod(method));
23+
#pragma warning restore CX001
1624
}
1725
}
1826
}

0 commit comments

Comments
 (0)