Repository navigation
Expand file tree
/
Copy pathVariantRewriter.cs
More file actions
134 lines (111 loc) · 6.32 KB
/
Copy pathVariantRewriter.cs
File metadata and controls
134 lines (111 loc) · 6.32 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
namespace Zomp.MethodCloning.Variants;
/// <summary>
/// Writes a method as one of its variants, swapping the word which names the variant wherever it
/// appears: in the method's name, in the types and static members it refers to, and in its
/// documentation. Writing <c>SaveAsBmp</c> as its <c>Gif</c> variant produces <c>SaveAsGif</c>,
/// which takes a <c>GifEncoder</c> where the original took a <c>BmpEncoder</c>.
/// </summary>
/// <param name="semanticModel">The semantic model.</param>
/// <param name="targetMethod">The method declaration to write a variant of.</param>
/// <param name="original">The word naming the variant the method was written for.</param>
/// <param name="variant">The word naming the variant to write.</param>
internal sealed class VariantRewriter(SemanticModel semanticModel, MethodDeclarationSyntax targetMethod, string original, string variant)
: CloningRewriter(semanticModel, targetMethod)
{
private readonly ImmutableArray<ReportedDiagnostic>.Builder diagnostics = ImmutableArray.CreateBuilder<ReportedDiagnostic>();
// Types kept for want of a counterpart, each reported once however often the method names it.
private readonly List<ISymbol> kept = [];
private Location? methodLocation;
/// <summary>
/// Gets the diagnostics reported while writing the variant.
/// </summary>
public ImmutableArray<ReportedDiagnostic> Diagnostics => diagnostics.ToImmutable();
/// <inheritdoc/>
public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
{
methodLocation = node.Identifier.GetLocation();
if (base.VisitMethodDeclaration(node) is not MethodDeclarationSyntax clone)
{
return null;
}
// The documentation names the variant in prose, which no symbol reaches. It is taken from
// the original, since dropping the attribute which marked the method can take it along.
var documentation = SyntaxFactory.ParseLeadingTrivia(Rename(node.GetLeadingTrivia().ToFullString(), original, variant));
var name = SyntaxFactory.Identifier(Rename(node.Identifier.ValueText, original, variant)).WithTriviaFrom(clone.Identifier);
// The base qualifies a return type written as a generic, nullable or qualified name, but
// leaves one written as a simple name, which may name the variant, as it is.
var returnType = node.ReturnType is IdentifierNameSyntax
? Qualify(node.ReturnType).WithTriviaFrom(clone.ReturnType)
: clone.ReturnType;
return clone
.WithIdentifier(name)
.WithReturnType(returnType)
.WithLeadingTrivia(RemovePreprocessorDirectives(documentation));
}
/// <summary>
/// Drops <c>[CloneVariants]</c>, which marks the method written by hand, not its variants.
/// </summary>
/// <param name="node">The attribute list.</param>
/// <returns>The list without the attribute, or null when nothing else is left in it.</returns>
public override SyntaxNode? VisitAttributeList(AttributeListSyntax node)
{
var visited = (AttributeListSyntax)base.VisitAttributeList(node)!;
var attributes = node.Attributes
.Zip(visited.Attributes, static (original, rewritten) => (original, rewritten))
.Where(pair => !IsCloneVariants(pair.original))
.Select(static pair => pair.rewritten);
var remaining = SyntaxFactory.SeparatedList(attributes);
return remaining.Count == 0 ? null : visited.WithAttributes(remaining);
}
/// <summary>
/// Swaps the word naming one variant for the word naming another.
/// </summary>
/// <param name="text">Text to swap the word in.</param>
/// <param name="from">The word naming the variant the text was written for.</param>
/// <param name="to">The word naming the variant to write.</param>
/// <returns>The text for the other variant.</returns>
internal static string Rename(string text, string from, string to) => text.Replace(from, to);
/// <summary>
/// Substitutes the counterpart of a type written for the original variant, found by swapping
/// the word in its namespace and name: <c>Formats.Bmp.BmpEncoder</c> becomes
/// <c>Formats.Gif.GifEncoder</c>. A type without a counterpart is kept, and reported.
/// </summary>
/// <param name="symbol">The symbol.</param>
/// <returns>The counterpart, or null to keep the symbol.</returns>
protected override SimpleNameSyntax? MapSymbol(ISymbol symbol)
{
if (symbol is not INamedTypeSymbol { ContainingNamespace: { } containingNamespace } type
|| type.Name.IndexOf(original, StringComparison.Ordinal) < 0)
{
return null;
}
var metadataName = containingNamespace.IsGlobalNamespace
? type.MetadataName
: $"{containingNamespace.ToDisplayString()}.{type.MetadataName}";
var counterpartName = Rename(metadataName, original, variant);
if (SemanticModel.Compilation.GetTypeByMetadataName(counterpartName) is { } counterpart
&& !SymbolEqualityComparer.Default.Equals(counterpart, type))
{
return Qualify(counterpart);
}
if (methodLocation is not null && !kept.Contains(type, SymbolEqualityComparer.Default))
{
kept.Add(type);
diagnostics.Add(ReportedDiagnostic.Create(
VariantDiagnostics.NoCounterpart,
methodLocation,
$"'{type.ToDisplayString()}' has no counterpart named '{counterpartName}'"));
}
return null;
}
/// <summary>
/// Refers to the counterpart of a static member written for the original variant, which may
/// be another variant: <c>SaveAsBmp(source, path, default)</c> becomes <c>SaveAsGif(...)</c>.
/// </summary>
/// <param name="member">The static field or method.</param>
/// <returns>The name of its counterpart.</returns>
protected override string MapMemberName(ISymbol member) => Rename(member.Name, original, variant);
private bool IsCloneVariants(AttributeSyntax attribute)
=> GetSymbol(attribute) is IMethodSymbol { ContainingType: { } type }
&& type.ToDisplayString() == VariantGenerator.AttributeName;
}