📄
Core/ReachabilityAnalyzer.cs
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; using Reacher.MemberCollectors; namespace Reacher; internal sealed class ReachabilityAnalyzer(IReadOnlyCollection<Compilation> compilations) { private readonly Dictionary<ISymbol, ReachabilityConfidence> reachableMembers = []; private readonly HashSet<ISymbol> implementableMembers = []; public void Analyze(ISymbol member, CancellationToken cancellationToken) => Analyze(member, ReachabilityConfidence.DefinitelyReachable, cancellationToken); private void Analyze( ISymbol member, ReachabilityConfidence reachabilityConfidence, CancellationToken cancellationToken ) { member = GetCanonicalMemberSymbol(member); if ( !IsUserDefined(member) || !TryGetAssociatedCompilation(member, out var compilation) || reachableMembers.TryGetValue(member, out var existingConfidence) || reachabilityConfidence < existingConfidence ) { return; } reachableMembers.Add(member, reachabilityConfidence); foreach (var declaration in member.DeclaringSyntaxReferences) { AnalyzeDeclaration(compilation, declaration, reachabilityConfidence, cancellationToken); } if (member.IsAbstract || member.IsVirtual) { implementableMembers.Add(member); } } private static ISymbol GetCanonicalMemberSymbol(ISymbol member) => member switch { IMethodSymbol { ReducedFrom: ISymbol unreduced } => unreduced, _ => member, }; private static bool IsUserDefined(ISymbol member) => !member.IsImplicitlyDeclared && (member.CanBeReferencedByName || member.Name is "<Main>$"); private bool TryGetAssociatedCompilation(ISymbol member, [NotNullWhen(true)] out Compilation? compilation) { compilation = compilations.FirstOrDefault(c => member.DeclaringSyntaxReferences.Any(d => !d.Span.IsEmpty && c.ContainsSyntaxTree(d.SyntaxTree)) ); return compilation is not null; } private void AnalyzeDeclaration( Compilation compilation, SyntaxReference declaration, ReachabilityConfidence reachabilityConfidence, CancellationToken cancellationToken ) { var originalNode = declaration.GetSyntax(cancellationToken); var expressions = originalNode .DescendantNodes(ShouldDescendInto(originalNode)) .OfType<ExpressionSyntax>() .Where(e => e is IdentifierNameSyntax or ImplicitObjectCreationExpressionSyntax or ObjectCreationExpressionSyntax ); var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); foreach (var identifier in expressions) { if (!semanticModel.IsReachable(identifier)) { continue; } if ( semanticModel.GetSymbolInfo(identifier, cancellationToken) is not { Symbol: ISymbol member } || member is not (IMethodSymbol or IPropertySymbol) ) { continue; } Analyze(member, reachabilityConfidence, cancellationToken); } } private static Func<SyntaxNode, bool> ShouldDescendInto(SyntaxNode originalNode) => syntaxNode => syntaxNode == originalNode || !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement); public ReachabilityAnalysis GetAnalysis(CancellationToken cancellationToken) { var unreachableMembers = AnalyzeUnreachableMembersMembers(cancellationToken); unreachableMembers = AnalyzeUnreachableMembersMembers(cancellationToken); return new([.. reachableMembers.Select(kvp => new ReachableMember(kvp.Key, kvp.Value))], unreachableMembers); } private HashSet<ISymbol> AnalyzeUnreachableMembersMembers(CancellationToken cancellationToken) { var unreachableMembers = GetUnreachableMembers(); AnalyzeProbablyReachableMembers(unreachableMembers, cancellationToken); unreachableMembers.ExceptWith(reachableMembers.Keys); return unreachableMembers; } private HashSet<ISymbol> GetUnreachableMembers() { var collector = new PredicateMembersCollector(m => IsUserDefined(m) && !reachableMembers.ContainsKey(m)); compilations.Accept(collector); return [.. collector.Members]; } private void AnalyzeProbablyReachableMembers( IReadOnlySet<ISymbol> unreachableMembers, CancellationToken cancellationToken ) { foreach (var member in unreachableMembers) { if (IsProbablyReachable(member)) { Analyze(member, ReachabilityConfidence.ProbablyReachable, cancellationToken); } } } private bool IsProbablyReachable(ISymbol member) { var possibleTransientTargets = member.ExplicitOrImplicitInterfaceImplementations(); if (member.GetOverriddenMember() is { } overriddenMember) { possibleTransientTargets.Add(overriddenMember); } return possibleTransientTargets.Any(reachableMembers.ContainsKey); } }