Skip to content

Commit 051939e

Browse files
committed
🌍 #298 Basic initial implementation of ConfigureGenWithMember code refactor
1 parent 9d72262 commit 051939e

File tree

11 files changed

+296
-14
lines changed

11 files changed

+296
-14
lines changed

GalaxyCheck.Xunit.CodeAnalysis.Debug.sln

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit.CodeAnaly
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Xunit.CodeAnalysis.Vsix", "src\GalaxyCheck.Xunit.CodeAnalysis.Vsix\GalaxyCheck.Xunit.CodeAnalysis.Vsix.csproj", "{3C66752B-BB09-434E-8316-F93ADE8CDE25}"
99
EndProject
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit.CodeAnalysis.Tests", "src\GalaxyCheck.Xunit.CodeAnalysis.Tests\GalaxyCheck.Xunit.CodeAnalysis.Tests.csproj", "{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +33,14 @@ Global
3133
{3C66752B-BB09-434E-8316-F93ADE8CDE25}.Release|Any CPU.Build.0 = Release|Any CPU
3234
{3C66752B-BB09-434E-8316-F93ADE8CDE25}.Release|x86.ActiveCfg = Release|x86
3335
{3C66752B-BB09-434E-8316-F93ADE8CDE25}.Release|x86.Build.0 = Release|x86
36+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Debug|Any CPU.Build.0 = Debug|Any CPU
38+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Debug|x86.ActiveCfg = Debug|Any CPU
39+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Debug|x86.Build.0 = Debug|Any CPU
40+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Release|x86.ActiveCfg = Release|Any CPU
43+
{7CC3A9CF-ED41-4A8A-BB7F-D77530A5C429}.Release|x86.Build.0 = Release|Any CPU
3444
EndGlobalSection
3545
GlobalSection(SolutionProperties) = preSolution
3646
HideSolutionNode = FALSE

src/GalaxyCheck.Xunit.CodeAnalysis.Tests/Analyzers/MemberGenShouldReferenceValidMemberTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.Threading.Tasks;
22
using Xunit;
3-
using Verifier = GalaxyCheck.Xunit.CodeAnalysis.Tests.CSharpVerifier<GalaxyCheck.Xunit.CodeAnalysis.MemberGenShouldReferenceValidMember>;
3+
using Verifier = GalaxyCheck.Xunit.CodeAnalysis.Tests.Analyzers.Verifier<GalaxyCheck.Xunit.CodeAnalysis.Analyzers.MemberGenShouldReferenceValidMember>;
44

55
namespace GalaxyCheck.Xunit.CodeAnalysis.Tests.CodeAnalysis
66
{

src/GalaxyCheck.Xunit.CodeAnalysis.Tests/CSharpVerifier.cs src/GalaxyCheck.Xunit.CodeAnalysis.Tests/Analyzers/Verifier.cs

+3-10
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
using Microsoft.CodeAnalysis.Diagnostics;
44
using Microsoft.CodeAnalysis.Testing;
55
using Microsoft.CodeAnalysis.Testing.Verifiers;
6-
using Microsoft.CodeAnalysis.Text;
7-
using System.Collections.Immutable;
8-
using System.Linq;
96
using System.Reflection;
107
using System.Threading.Tasks;
118
using Xunit;
129

13-
namespace GalaxyCheck.Xunit.CodeAnalysis.Tests
10+
namespace GalaxyCheck.Xunit.CodeAnalysis.Tests.Analyzers
1411
{
15-
public class CSharpVerifier<TAnalyzer>
12+
public class Verifier<TAnalyzer>
1613
where TAnalyzer : DiagnosticAnalyzer, new()
1714
{
1815
public static DiagnosticResult Diagnostic(string diagnosticId)
@@ -24,7 +21,7 @@ public static Task Verify(
2421
string[] sources,
2522
params DiagnosticResult[] diagnostics)
2623
{
27-
var test = new Test();
24+
var test = new CSharpCodeFixTest<TAnalyzer, EmptyCodeFixProvider, XUnitVerifier>();
2825

2926
foreach (var source in sources)
3027
test.TestState.Sources.Add(source);
@@ -40,9 +37,5 @@ public static Task Verify(
4037

4138
return test.RunAsync();
4239
}
43-
44-
public class Test : CSharpCodeFixTest<TAnalyzer, EmptyCodeFixProvider, XUnitVerifier>
45-
{
46-
}
4740
}
4841
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Microsoft.CodeAnalysis.Text;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
using Verifier = GalaxyCheck.Xunit.CodeAnalysis.Tests.CodeRefactoringProviders.Verifier<GalaxyCheck.Xunit.CodeAnalysis.CodeRefactoringProviders.ConfigureGenWithMember>;
5+
6+
namespace GalaxyCheck.Xunit.CodeAnalysis.Tests.CodeRefactoringProviders
7+
{
8+
/// <summary>
9+
/// - Variables:
10+
/// - Line position (anywhere within "int x")
11+
/// - Parameter name/method name, property becomes "[MethodName]_[ParameterName]"
12+
/// - Don't add member gen attribute if it already exists
13+
/// - Handle Lists with explicit generators, and other higher-order generators (nullable etc.)
14+
/// - Provide the refactor anywhere inside the method declaration node, having it provide an option for each argument
15+
/// </summary>
16+
public class ConfigureGenWithMemberTests
17+
{
18+
[Theory]
19+
[InlineData("byte", "Gen.Byte()")]
20+
[InlineData("short", "Gen.Int16()")]
21+
[InlineData("int", "Gen.Int32()")]
22+
[InlineData("long", "Gen.Int64()")]
23+
[InlineData("string", "Gen.String()")]
24+
[InlineData("char", "Gen.Char()")]
25+
[InlineData("bool", "Gen.Boolean()")]
26+
[InlineData("System.Guid", "Gen.Guid()")]
27+
[InlineData("System.DateTime", "Gen.DateTime()")]
28+
[InlineData("TestClass", "Gen.Create<TestClass>()")] // A random type, not generatable by default, which is in scope
29+
public async Task ItInsertsAndReferencesTheMember(string type, string genExpression)
30+
{
31+
var code = @"
32+
using GalaxyCheck;
33+
34+
public class TestClass
35+
{
36+
[Property]
37+
public void TestMethod(" + type + @" x)
38+
{
39+
}
40+
}
41+
";
42+
var codeProvidingRefactorPosition = new LinePosition(7, 28);
43+
44+
var expectedRefactoredCode = @"
45+
using GalaxyCheck;
46+
47+
public class TestClass
48+
{
49+
private static IGen<" + type + @"> TestMethod_x => " + genExpression + @";
50+
51+
[Property]
52+
public void TestMethod([MemberGen(nameof(TestMethod_x))] " + type + @" x)
53+
{
54+
}
55+
}
56+
";
57+
58+
var expectedRefactoringTitle = $"Configure generation of parameter with MemberGenAttribute";
59+
60+
await Verifier.Verify(code, codeProvidingRefactorPosition, expectedRefactoredCode, expectedRefactoringTitle);
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeRefactorings;
3+
using Microsoft.CodeAnalysis.CSharp.Testing;
4+
using Microsoft.CodeAnalysis.Testing;
5+
using Microsoft.CodeAnalysis.Testing.Verifiers;
6+
using Microsoft.CodeAnalysis.Text;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Threading.Tasks;
10+
using Xunit;
11+
12+
namespace GalaxyCheck.Xunit.CodeAnalysis.Tests.CodeRefactoringProviders
13+
{
14+
public static class Verifier<TCodeRefactoringProvider>
15+
where TCodeRefactoringProvider : CodeRefactoringProvider, new()
16+
{
17+
public static Task Verify(
18+
string code,
19+
string codeProvidingRefactor,
20+
string expectedRefactoredCode,
21+
string expectedRefactoringTitle)
22+
{
23+
var linePosition = new LinePosition(1, 1);
24+
25+
var indexOfCodeRefactoringProvider = code.IndexOf(codeProvidingRefactor);
26+
27+
28+
return Verify(code, linePosition, expectedRefactoredCode, expectedRefactoringTitle);
29+
}
30+
31+
public static async Task Verify(
32+
string code,
33+
LinePosition codeProvidingRefactorPosition,
34+
string expectedRefactoredCode,
35+
string expectedRefactoringTitle)
36+
{
37+
var test = new CSharpCodeRefactoringTest<TCodeRefactoringProvider, XUnitVerifier>()
38+
{
39+
TestCode = code,
40+
FixedCode = expectedRefactoredCode,
41+
CodeActionVerifier = (codeAction, verifier) =>
42+
{
43+
verifier.Equal(codeAction.Title, expectedRefactoringTitle);
44+
}
45+
};
46+
47+
test.ExpectedDiagnostics.Add(new DiagnosticResult("Refactoring", DiagnosticSeverity.Hidden)
48+
.WithSpan(codeProvidingRefactorPosition.Line, codeProvidingRefactorPosition.Character, codeProvidingRefactorPosition.Line, codeProvidingRefactorPosition.Character));
49+
50+
test.OffersEmptyRefactoring = true;
51+
52+
test.TestState.AdditionalReferences.AddRange(new[]
53+
{
54+
MetadataReference.CreateFromFile(Assembly.GetAssembly(typeof(FactAttribute))!.Location),
55+
MetadataReference.CreateFromFile(Assembly.GetAssembly(typeof(IGen))!.Location),
56+
MetadataReference.CreateFromFile(Assembly.GetAssembly(typeof(PropertyAttribute))!.Location)
57+
});
58+
59+
await test.RunAsync();
60+
}
61+
}
62+
}

src/GalaxyCheck.Xunit.CodeAnalysis.Tests/GalaxyCheck.Xunit.CodeAnalysis.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.0" />
12+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing.XUnit" Version="1.1.0" />
1213
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
1314
<PackageReference Include="xunit" Version="2.4.1" />
1415
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

src/GalaxyCheck.Xunit.CodeAnalysis.Vsix/source.extension.vsixmanifest

+1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
<Assets>
2020
<Asset Type="Microsoft.VisualStudio.VsPackage" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%;PkgdefProjectOutputGroup|" />
2121
<Asset Type="Microsoft.VisualStudio.Analyzer" d:Source="Project" d:ProjectName="GalaxyCheck.Xunit.CodeAnalysis" Path="|GalaxyCheck.Xunit.CodeAnalysis|" />
22+
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="GalaxyCheck.Xunit.CodeAnalysis" Path="|GalaxyCheck.Xunit.CodeAnalysis|" />
2223
</Assets>
2324
</PackageManifest>

src/GalaxyCheck.Xunit.CodeAnalysis/Descriptors.cs src/GalaxyCheck.Xunit.CodeAnalysis/Analyzers/Descriptors.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Microsoft.CodeAnalysis;
22

3-
namespace GalaxyCheck.Xunit.CodeAnalysis
3+
namespace GalaxyCheck.Xunit.CodeAnalysis.Analyzers
44
{
55
public enum Category
66
{

src/GalaxyCheck.Xunit.CodeAnalysis/MemberGenShouldReferenceValidMember.cs src/GalaxyCheck.Xunit.CodeAnalysis/Analyzers/MemberGenShouldReferenceValidMember.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Collections.Immutable;
77
using System.Linq;
88

9-
namespace GalaxyCheck.Xunit.CodeAnalysis
9+
namespace GalaxyCheck.Xunit.CodeAnalysis.Analyzers
1010
{
1111
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1212
public class MemberGenShouldReferenceValidMember : DiagnosticAnalyzer
@@ -33,7 +33,7 @@ private static void AnalyzeCompilation(CompilationStartAnalysisContext compilati
3333

3434
private static void AnalyzeAttribute(Compilation compilation, SyntaxNodeAnalysisContext context)
3535
{
36-
var memberGenAttributeType = compilation.GetTypeByMetadataName("GalaxyCheck.MemberGenAttribute");
36+
var memberGenAttributeType = compilation.TryGetMemberGenAttributeType();
3737
if (memberGenAttributeType == null)
3838
{
3939
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeActions;
3+
using Microsoft.CodeAnalysis.CodeRefactorings;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Editing;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
10+
11+
namespace GalaxyCheck.Xunit.CodeAnalysis.CodeRefactoringProviders
12+
{
13+
[ExportCodeRefactoringProvider(LanguageNames.CSharp)]
14+
public class ConfigureGenWithMember : CodeRefactoringProvider
15+
{
16+
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
17+
{
18+
var document = context.Document;
19+
20+
var root = await document.GetSyntaxRootAsync(context.CancellationToken);
21+
if (root is null) return;
22+
23+
var semanticModel = await document.GetSemanticModelAsync(context.CancellationToken);
24+
if (semanticModel is null) return;
25+
26+
var propertyAttributeType = semanticModel.Compilation.TryGetPropertyAttributeType();
27+
if (propertyAttributeType is null) return;
28+
29+
var activeNode = root.FindNode(context.Span);
30+
31+
var activeMethodDeclarationSyntax = activeNode.FirstAncestorOrSelf<MethodDeclarationSyntax>();
32+
if (activeMethodDeclarationSyntax is null) return;
33+
34+
var propertyAttribute = activeMethodDeclarationSyntax
35+
.AttributeLists
36+
.SelectMany(l => l.Attributes)
37+
.FirstOrDefault(a =>
38+
{
39+
var attributeSymbol = semanticModel.GetSymbolInfo(a);
40+
return SymbolEqualityComparer.Default.Equals(attributeSymbol.Symbol?.ContainingType, propertyAttributeType);
41+
});
42+
if (propertyAttribute is null) return;
43+
44+
var activeParameterSyntax = activeNode.FirstAncestorOrSelf<ParameterSyntax>();
45+
if (activeParameterSyntax is not null)
46+
{
47+
context.RegisterRefactoring(InsertAndReferenceMemberGen(
48+
semanticModel,
49+
document,
50+
activeMethodDeclarationSyntax,
51+
activeParameterSyntax));
52+
}
53+
}
54+
55+
private static CodeAction InsertAndReferenceMemberGen(
56+
SemanticModel semanticModel,
57+
Document document,
58+
MethodDeclarationSyntax methodDeclarationSyntax,
59+
ParameterSyntax parameterSyntax)
60+
{
61+
var compilation = semanticModel.Compilation;
62+
var title = "Configure generation of parameter with MemberGenAttribute";
63+
64+
var genGenericType = compilation.GetTypeByMetadataName("GalaxyCheck.IGen`1");
65+
var parameterType = semanticModel.GetDeclaredSymbol(parameterSyntax)!.Type;
66+
var genType = genGenericType!.Construct(parameterType);
67+
68+
var genFactoryType = compilation.GetTypeByMetadataName("GalaxyCheck.Gen")!;
69+
70+
return CodeAction.Create(title, async cancellationToken =>
71+
{
72+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
73+
var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
74+
75+
var memberName = $"{methodDeclarationSyntax.Identifier.Text}_{parameterSyntax.Identifier.Text}";
76+
77+
var propertySyntax = CreateProperty(syntaxGenerator, memberName, genType, parameterType);
78+
var attributeSyntax = CreateMemberGenAttribute(syntaxGenerator, memberName);
79+
80+
editor.InsertBefore(methodDeclarationSyntax, propertySyntax);
81+
editor.ReplaceNode(parameterSyntax, parameterSyntax.AddAttributeLists(attributeSyntax));
82+
return editor.GetChangedDocument();
83+
});
84+
}
85+
86+
private static PropertyDeclarationSyntax CreateProperty(SyntaxGenerator syntaxGenerator, string name, ITypeSymbol genType, ITypeSymbol parameterType)
87+
{
88+
var propertyType = syntaxGenerator.TypeExpression(genType);
89+
90+
var propertyExpression = parameterType.Name switch
91+
{
92+
"Boolean" => SimpleBuiltInGenInvocation("Boolean"),
93+
"Byte" => SimpleBuiltInGenInvocation("Byte"),
94+
"Int16" => SimpleBuiltInGenInvocation("Int16"),
95+
"Int32" => SimpleBuiltInGenInvocation("Int32"),
96+
"Int64" => SimpleBuiltInGenInvocation("Int64"),
97+
"Char" => SimpleBuiltInGenInvocation("Char"),
98+
"String" => SimpleBuiltInGenInvocation("String"),
99+
"DateTime" => SimpleBuiltInGenInvocation("DateTime"),
100+
"Guid" => SimpleBuiltInGenInvocation("Guid"),
101+
_ => InvocationExpression(
102+
MemberAccessExpression(
103+
SyntaxKind.SimpleMemberAccessExpression,
104+
IdentifierName("Gen"),
105+
GenericName(
106+
Identifier("Create"))
107+
.WithTypeArgumentList(
108+
TypeArgumentList(
109+
SingletonSeparatedList<TypeSyntax>(IdentifierName(parameterType.Name))))))
110+
};
111+
112+
var propertyValue = ArrowExpressionClause(propertyExpression);
113+
114+
return ((PropertyDeclarationSyntax)syntaxGenerator.PropertyDeclaration(
115+
name,
116+
propertyType,
117+
Accessibility.Private,
118+
DeclarationModifiers.Static | DeclarationModifiers.ReadOnly,
119+
getAccessorStatements: null,
120+
setAccessorStatements: null))
121+
.WithAccessorList(null)
122+
.WithExpressionBody(propertyValue)
123+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
124+
}
125+
126+
private static InvocationExpressionSyntax SimpleBuiltInGenInvocation(string methodName)
127+
{
128+
return InvocationExpression(
129+
MemberAccessExpression(
130+
SyntaxKind.SimpleMemberAccessExpression,
131+
IdentifierName("Gen"),
132+
IdentifierName(methodName)));
133+
}
134+
135+
private static AttributeListSyntax CreateMemberGenAttribute(SyntaxGenerator syntaxGenerator, string memberName) => (AttributeListSyntax)syntaxGenerator.Attribute(
136+
"MemberGen",
137+
syntaxGenerator.NameOfExpression(syntaxGenerator.IdentifierName(memberName)));
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace GalaxyCheck.Xunit.CodeAnalysis
4+
{
5+
internal static class CompilationExtensions
6+
{
7+
public static INamedTypeSymbol? TryGetPropertyAttributeType(this Compilation compilation) =>
8+
compilation.GetTypeByMetadataName("GalaxyCheck.PropertyAttribute");
9+
10+
public static INamedTypeSymbol? TryGetMemberGenAttributeType(this Compilation compilation) =>
11+
compilation.GetTypeByMetadataName("GalaxyCheck.MemberGenAttribute");
12+
}
13+
}

0 commit comments

Comments
 (0)