Skip to content

Commit d1cf356

Browse files
author
RandomCrocodile
committed
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.
1 parent ae10daa commit d1cf356

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.CodeAnalysis.Testing;
3+
using Xunit;
4+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
5+
Confuser.Analyzers.ResolveThrowAuditAnalyzer,
6+
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
7+
8+
namespace Confuser.Analyzers.Test {
9+
public class ResolveThrowAuditAnalyzerTest {
10+
[Theory]
11+
[InlineData("ResolveThrow")]
12+
[InlineData("ResolveTypeDefThrow")]
13+
[InlineData("ResolveMethodDefThrow")]
14+
[InlineData("ResolveFieldThrow")]
15+
public async Task Flags_ResolveThrowHelpers(string methodName) {
16+
string source = @"
17+
class Ref { public object " + methodName + @"() => null; }
18+
class C {
19+
void M(Ref r) {
20+
var x = r.{|#0:" + methodName + @"|}();
21+
}
22+
}";
23+
var expected = Verify.Diagnostic("CX003").WithLocation(0).WithArguments(methodName);
24+
await Verify.VerifyAnalyzerAsync(source, expected);
25+
}
26+
27+
[Fact]
28+
public async Task DoesNotFlag_NonThrowingResolve() {
29+
const string source = @"
30+
class Ref { public object ResolveTypeDef() => null; }
31+
class C {
32+
void M(Ref r) {
33+
var x = r.ResolveTypeDef();
34+
}
35+
}";
36+
await Verify.VerifyAnalyzerAsync(source);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)