-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathAnnotationReachabilityTrimmer.cs
More file actions
406 lines (357 loc) · 15.7 KB
/
Copy pathAnnotationReachabilityTrimmer.cs
File metadata and controls
406 lines (357 loc) · 15.7 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
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Light.GuardClauses.SourceCodeTransformation;
/// <summary>
/// Removes the bundled support types that the generated file does not reference any longer. This step must
/// run after <see cref="CleanupStep.RemoveAnnotations" /> because only the merged tree shows which annotation
/// usages actually survive cleanup - the original source files still contain every removable usage.
/// </summary>
internal static class AnnotationReachabilityTrimmer
{
private const string JetBrainsAnnotationsNamespace = "JetBrains.Annotations";
private const string NotNullAttributeName = "NotNullAttribute";
private const string ValidatedNotNullAttributeName = "ValidatedNotNullAttribute";
private static readonly HashSet<string> BundledNamespaceNames = new (StringComparer.Ordinal)
{
JetBrainsAnnotationsNamespace,
"System.Diagnostics.CodeAnalysis",
"System.Runtime.CompilerServices",
};
public static CompilationUnitSyntax Trim(CompilationUnitSyntax compilationUnit, SourceFileMergeOptions options)
{
ArgumentNullException.ThrowIfNull(compilationUnit);
ArgumentNullException.ThrowIfNull(options);
// At this point in the pipeline the root does not belong to a syntax tree that reflects it, thus the
// tree must be recreated with the export's parse options before anything can be bound. Creating the
// tree clones the root, so all further work happens on the tree's root.
var syntaxTree = CSharpSyntaxTree.Create(
compilationUnit,
SourceReachabilityAnalyzer.CreateParseOptions(options.TargetFramework)
);
var root = (CompilationUnitSyntax) syntaxTree.GetRoot();
var catalog = BundledTypeCatalog.Create(root, options);
var reachableTypes = catalog.Types.Count == 0 ?
new () :
DetermineReachableTypes(syntaxTree, root, catalog);
root = RemoveUnreachableTypes(root, catalog, reachableTypes);
return AdjustJetBrainsUsingDirectives(root, options, reachableTypes);
}
private static HashSet<BundledType> DetermineReachableTypes(
SyntaxTree syntaxTree,
CompilationUnitSyntax root,
BundledTypeCatalog catalog
)
{
var compilation = CSharpCompilation.Create(
"Light.GuardClauses.SourceExportAnnotationReachability",
[syntaxTree],
SourceReachabilityAnalyzer.GetMetadataReferences(),
new (
OutputKind.DynamicallyLinkedLibrary,
nullableContextOptions: NullableContextOptions.Enable,
allowUnsafe: true
)
);
var semanticModel = compilation.GetSemanticModel(syntaxTree, true);
var typesBySymbol = new Dictionary<ISymbol, BundledType>(SymbolEqualityComparer.Default);
foreach (var bundledType in catalog.Types)
{
if (GetDeclaredSymbol(semanticModel, bundledType.Declaration) is { } typeSymbol)
{
typesBySymbol[typeSymbol] = bundledType;
}
}
var reachableTypes = new HashSet<BundledType>();
var typesToScan = new Queue<BundledType>();
// Reachability is established from attribute usages and using aliases. Any future bundled type must
// be reachable through one of these two - a type that is referenced in another position (a base type,
// a member signature, a typeof expression) would be trimmed although it is still used. The only cref
// references among the bundled types are self-references that disappear with their own declaration.
foreach (var attribute in root.DescendantNodes().OfType<AttributeSyntax>())
{
if (catalog.IsDeclaredInsideBundledType(attribute))
{
continue;
}
MarkAttributeType(attribute);
}
// A using alias names its target type, thus an alias that outlives its target does not compile even
// when nothing else refers to the alias.
foreach (var usingDirective in root.Usings)
{
if (usingDirective.Alias != null)
{
MarkAliasedType(usingDirective);
}
}
while (typesToScan.TryDequeue(out var scannedType))
{
foreach (var attribute in scannedType.Declaration.DescendantNodes().OfType<AttributeSyntax>())
{
MarkAttributeType(attribute);
}
}
return reachableTypes;
void MarkAttributeType(AttributeSyntax attribute)
{
var attributeType = ResolveAttributeType(semanticModel, attribute);
if (attributeType == null)
{
// A degraded binding must produce a slightly larger file, never one that fails to compile.
foreach (var matchingType in catalog.FindByUnqualifiedName(attribute.Name))
{
Mark(matchingType);
}
return;
}
if (typesBySymbol.TryGetValue(attributeType, out var bundledType))
{
Mark(bundledType);
}
}
void MarkAliasedType(UsingDirectiveSyntax usingDirective)
{
var aliasedType = GetAttributeType(semanticModel.GetSymbolInfo(usingDirective.NamespaceOrType).Symbol);
if (aliasedType is not null and not IErrorTypeSymbol)
{
if (typesBySymbol.TryGetValue(aliasedType, out var bundledType))
{
Mark(bundledType);
}
return;
}
if (usingDirective.NamespaceOrType is NameSyntax aliasedName)
{
foreach (var matchingType in catalog.FindByUnqualifiedName(aliasedName))
{
Mark(matchingType);
}
}
}
void Mark(BundledType bundledType)
{
if (reachableTypes.Add(bundledType))
{
typesToScan.Enqueue(bundledType);
}
}
}
private static INamedTypeSymbol? ResolveAttributeType(SemanticModel semanticModel, AttributeSyntax attribute)
{
var symbolInfo = semanticModel.GetSymbolInfo(attribute);
var attributeType = GetAttributeType(symbolInfo.Symbol);
if (attributeType == null)
{
foreach (var candidateSymbol in symbolInfo.CandidateSymbols)
{
attributeType = GetAttributeType(candidateSymbol);
if (attributeType != null)
{
break;
}
}
}
attributeType ??= GetAttributeType(semanticModel.GetSymbolInfo(attribute.Name).Symbol);
return attributeType is null or IErrorTypeSymbol ? null : attributeType;
}
private static INamedTypeSymbol? GetAttributeType(ISymbol? symbol) =>
symbol switch
{
IMethodSymbol methodSymbol => methodSymbol.ContainingType,
INamedTypeSymbol namedTypeSymbol => namedTypeSymbol,
_ => null,
};
private static CompilationUnitSyntax RemoveUnreachableTypes(
CompilationUnitSyntax root,
BundledTypeCatalog catalog,
HashSet<BundledType> reachableTypes
)
{
var nodesToRemove = new List<SyntaxNode>();
foreach (var namespaceDeclaration in catalog.BundledNamespaces)
{
var unreachableDeclarations = new List<SyntaxNode>();
var retainsMember = false;
foreach (var member in namespaceDeclaration.Members)
{
// Anything that is not a bundled type counts as retained: only the catalog decides what may
// be removed for lack of references.
if (catalog.TryGetBundledType(member, out var bundledType) && !reachableTypes.Contains(bundledType))
{
unreachableDeclarations.Add(member);
}
else
{
retainsMember = true;
}
}
if (retainsMember)
{
nodesToRemove.AddRange(unreachableDeclarations);
}
else
{
// Removing the namespace node also removes its license banner: the banner is leading trivia
// of the namespace token.
nodesToRemove.Add(namespaceDeclaration);
}
}
foreach (var bundledType in catalog.TypesOutsideBundledNamespaces)
{
if (!reachableTypes.Contains(bundledType))
{
nodesToRemove.Add(bundledType.Declaration);
}
}
return nodesToRemove.Count == 0 ?
root :
root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia)!;
}
private static CompilationUnitSyntax AdjustJetBrainsUsingDirectives(
CompilationUnitSyntax root,
SourceFileMergeOptions options,
HashSet<BundledType> reachableTypes
)
{
// When the JetBrains annotations are not bundled, IncludeJetBrainsAnnotationsUsing keeps its meaning
// so that consumers can supply the annotations from an externally referenced package.
if (!options.IncludeJetBrainsAnnotations)
{
return root;
}
var retainsJetBrainsTypes = false;
var retainsJetBrainsNotNullAttribute = false;
foreach (var bundledType in reachableTypes)
{
if (bundledType.NamespaceName != JetBrainsAnnotationsNamespace)
{
continue;
}
retainsJetBrainsTypes = true;
retainsJetBrainsNotNullAttribute |= bundledType.Name == NotNullAttributeName;
}
var directivesToRemove = new List<UsingDirectiveSyntax>();
foreach (var usingDirective in root.Usings)
{
// The alias only exists to disambiguate the two NotNullAttribute declarations. Removing the
// namespace while leaving its import in place would be a compile error, thus both decisions
// are derived from the trimmed set together.
if (usingDirective.Alias != null)
{
if (!retainsJetBrainsNotNullAttribute &&
usingDirective.Alias.Name.Identifier.ValueText == NotNullAttributeName)
{
directivesToRemove.Add(usingDirective);
}
}
else if (!retainsJetBrainsTypes &&
usingDirective.NamespaceOrType.ToString() == JetBrainsAnnotationsNamespace)
{
directivesToRemove.Add(usingDirective);
}
}
return directivesToRemove.Count == 0 ?
root :
root.RemoveNodes(directivesToRemove, SyntaxRemoveOptions.KeepNoTrivia)!;
}
private static ISymbol? GetDeclaredSymbol(SemanticModel semanticModel, MemberDeclarationSyntax declaration) =>
declaration switch
{
BaseTypeDeclarationSyntax typeDeclaration => semanticModel.GetDeclaredSymbol(typeDeclaration),
DelegateDeclarationSyntax delegateDeclaration => semanticModel.GetDeclaredSymbol(delegateDeclaration),
_ => null,
};
private sealed record BundledType(MemberDeclarationSyntax Declaration, string NamespaceName, string Name);
/// <summary>
/// Identifies the bundled support types in the merged tree. They cannot be derived from the mechanisms
/// that brought them there - two of them arrive as always-processed source files that whitelist mode
/// exempts from reachability analysis, the other two as literal blocks injected by the merger.
/// </summary>
private sealed class BundledTypeCatalog
{
private readonly List<NamespaceDeclarationSyntax> _bundledNamespaces = new ();
private readonly List<BundledType> _types = new ();
private readonly Dictionary<SyntaxNode, BundledType> _typesByDeclaration = new ();
private readonly Dictionary<string, List<BundledType>> _typesByUnqualifiedName = new (StringComparer.Ordinal);
private readonly List<BundledType> _typesOutsideBundledNamespaces = new ();
public IReadOnlyList<BundledType> Types => _types;
public IReadOnlyList<NamespaceDeclarationSyntax> BundledNamespaces => _bundledNamespaces;
public IReadOnlyList<BundledType> TypesOutsideBundledNamespaces => _typesOutsideBundledNamespaces;
public static BundledTypeCatalog Create(CompilationUnitSyntax root, SourceFileMergeOptions options)
{
var catalog = new BundledTypeCatalog();
foreach (var namespaceDeclaration in root.Members.OfType<NamespaceDeclarationSyntax>())
{
var namespaceName = namespaceDeclaration.Name.ToString();
if (BundledNamespaceNames.Contains(namespaceName))
{
catalog._bundledNamespaces.Add(namespaceDeclaration);
foreach (var member in namespaceDeclaration.Members)
{
catalog.Add(member, namespaceName, false);
}
}
else if (namespaceName == options.BaseNamespace)
{
foreach (var member in namespaceDeclaration.Members)
{
if (member is BaseTypeDeclarationSyntax
{
Identifier.ValueText: ValidatedNotNullAttributeName,
})
{
catalog.Add(member, namespaceName, true);
}
}
}
}
return catalog;
}
public bool TryGetBundledType(
SyntaxNode declaration,
[MaybeNullWhen(false)] out BundledType bundledType
) =>
_typesByDeclaration.TryGetValue(declaration, out bundledType);
public bool IsDeclaredInsideBundledType(SyntaxNode node) =>
node.Ancestors().Any(_typesByDeclaration.ContainsKey);
public IReadOnlyList<BundledType> FindByUnqualifiedName(NameSyntax attributeName) =>
_typesByUnqualifiedName.TryGetValue(
CleanupStep.GetUnqualifiedAttributeName(attributeName),
out var bundledTypes
) ?
bundledTypes :
[];
private void Add(MemberDeclarationSyntax declaration, string namespaceName, bool isOutsideBundledNamespace)
{
var name = declaration switch
{
BaseTypeDeclarationSyntax typeDeclaration => typeDeclaration.Identifier.ValueText,
DelegateDeclarationSyntax delegateDeclaration => delegateDeclaration.Identifier.ValueText,
_ => null,
};
if (name == null)
{
return;
}
var bundledType = new BundledType(declaration, namespaceName, name);
_types.Add(bundledType);
_typesByDeclaration.Add(declaration, bundledType);
if (isOutsideBundledNamespace)
{
_typesOutsideBundledNamespaces.Add(bundledType);
}
var unqualifiedName = CleanupStep.RemoveAttributeSuffix(name);
if (!_typesByUnqualifiedName.TryGetValue(unqualifiedName, out var bundledTypes))
{
bundledTypes = new ();
_typesByUnqualifiedName.Add(unqualifiedName, bundledTypes);
}
bundledTypes.Add(bundledType);
}
}
}