forked from mkaring/ConfuserEx
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathUnhandledReflectionTypeLoadAnalyzer.cs
More file actions
104 lines (89 loc) · 4.43 KB
/
Copy pathUnhandledReflectionTypeLoadAnalyzer.cs
File metadata and controls
104 lines (89 loc) · 4.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Confuser.Analyzers {
/// <summary>
/// CX004 — flags <c>Assembly.GetTypes()</c> / <c>Module.GetTypes()</c> that are not guarded
/// against <see cref="System.Reflection.ReflectionTypeLoadException" />. That exception is
/// thrown whenever a contained type cannot be loaded (common for plugin assemblies with
/// unresolved dependencies) and caused packer/plugin startup crashes.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UnhandledReflectionTypeLoadAnalyzer : DiagnosticAnalyzer {
static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticIds.UnhandledReflectionTypeLoad,
"GetTypes() without ReflectionTypeLoadException handling",
"'{0}.GetTypes()' can throw ReflectionTypeLoadException when a contained type is unresolvable; wrap it in a try/catch",
DiagnosticIds.Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Assembly.GetTypes() and Module.GetTypes() throw ReflectionTypeLoadException when any " +
"contained type cannot be loaded. Guard the call (and prefer ex.Types on the exception) to avoid " +
"startup crashes when loading plugin or external assemblies.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context) {
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
}
static void Analyze(SyntaxNodeAnalysisContext context) {
var invocation = (InvocationExpressionSyntax)context.Node;
if (invocation.Expression is not MemberAccessExpressionSyntax member ||
member.Name.Identifier.ValueText != "GetTypes")
return;
if (context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is not IMethodSymbol method ||
method.Name != "GetTypes" || !method.Parameters.IsEmpty)
return;
var containingType = method.ContainingType?.ToDisplayString();
if (containingType != "System.Reflection.Assembly" && containingType != "System.Reflection.Module")
return;
var rtle = context.Compilation.GetTypeByMetadataName("System.Reflection.ReflectionTypeLoadException");
if (IsGuarded(invocation, context.SemanticModel, rtle, context.CancellationToken))
return;
context.ReportDiagnostic(Diagnostic.Create(Rule, member.Name.GetLocation(), containingType));
}
static bool IsGuarded(SyntaxNode node, SemanticModel model, INamedTypeSymbol? rtle, System.Threading.CancellationToken ct) {
for (var current = node.Parent; current != null; current = current.Parent) {
if (current is TryStatementSyntax tryStmt &&
tryStmt.Block.Span.Contains(node.Span) &&
CatchesReflectionTypeLoad(tryStmt, model, rtle, ct))
return true;
// Do not walk past the enclosing method / lambda / local-function boundary.
if (current is BaseMethodDeclarationSyntax ||
current is AnonymousFunctionExpressionSyntax ||
current is LocalFunctionStatementSyntax)
break;
}
return false;
}
static bool CatchesReflectionTypeLoad(TryStatementSyntax tryStmt, SemanticModel model, INamedTypeSymbol? rtle,
System.Threading.CancellationToken ct) {
foreach (var clause in tryStmt.Catches) {
// A general 'catch { }' (no declared type) catches everything.
if (clause.Declaration is null)
return true;
var caught = model.GetTypeInfo(clause.Declaration.Type, ct).Type as INamedTypeSymbol;
if (caught is null)
continue;
// The catch guards the call if ReflectionTypeLoadException is assignable to the caught
// type (i.e. the caught type is RTLE or one of its base types such as SystemException /
// Exception). If we cannot resolve RTLE, fall back to name matching on the base chain.
if (rtle is not null) {
for (var t = rtle; t is not null; t = t.BaseType) {
if (SymbolEqualityComparer.Default.Equals(t, caught))
return true;
}
}
else {
var name = caught.ToDisplayString();
if (name == "System.Exception" || name == "System.SystemException" ||
name == "System.Reflection.ReflectionTypeLoadException")
return true;
}
}
return false;
}
}
}