Skip to content

Commit fb4d102

Browse files
committed
source generator: analyzer + codefix for partial classes
1 parent 1ff7dff commit fb4d102

File tree

16 files changed

+311
-21
lines changed

16 files changed

+311
-21
lines changed

FastCloner.Benchmark/FastCloner.Benchmark.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BenchmarkDotNet" Version="0.15.0" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.15.1" />
1313
<PackageReference Include="DeepCloner" Version="0.10.4" />
1414
<PackageReference Include="DeepCopier" Version="1.0.4" />
1515
<PackageReference Include="DeepCopy" Version="1.0.3" />
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>preview</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
11+
<PrivateAssets>all</PrivateAssets>
12+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
13+
</PackageReference>
14+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
15+
</ItemGroup>
16+
<ItemGroup>
17+
<ProjectReference Include="..\FastCloner.SourceGenerator.Shared\FastCloner.SourceGenerator.Shared.csproj" />
18+
</ItemGroup>
19+
</Project>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using FastCloner.SourceGenerator.Shared;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
namespace FastCloner.SourceGenerator.Analyzers;
11+
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public class FastClonerClonableAnalyzer : DiagnosticAnalyzer
14+
{
15+
public const string DiagnosticId = "FC0001";
16+
private const string Title = "FastClonerClonable class must be partial";
17+
private const string MessageFormat = "The class '{0}' is marked with [FastClonerClonable] but is not partial";
18+
private const string Description = "Classes marked with [FastClonerClonable] must be partial to allow the source generator to add the cloning implementation.";
19+
private const string Category = "Usage";
20+
21+
private static readonly DiagnosticDescriptor rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ rule ];
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
context.EnableConcurrentExecution();
28+
context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
29+
}
30+
31+
private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
32+
{
33+
ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)context.Node;
34+
ISymbol? classSymbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, classDeclaration);
35+
36+
if (classSymbol is null)
37+
{
38+
return;
39+
}
40+
41+
bool hasClonableAttribute = classSymbol.GetAttributes().Any(x => string.Equals(x.AttributeClass?.Name, nameof(FastClonerClonableAttribute), StringComparison.OrdinalIgnoreCase));
42+
43+
if (!hasClonableAttribute)
44+
{
45+
return;
46+
}
47+
48+
bool isPartial = classDeclaration.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword));
49+
50+
if (!isPartial)
51+
{
52+
Diagnostic diagnostic = Diagnostic.Create(rule, classDeclaration.Identifier.GetLocation(), classSymbol.Name);
53+
context.ReportDiagnostic(diagnostic);
54+
}
55+
}
56+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>preview</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
11+
<PrivateAssets>all</PrivateAssets>
12+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
13+
</PackageReference>
14+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
16+
</ItemGroup>
17+
<ItemGroup>
18+
<ProjectReference Include="..\FastCloner.SourceGenerator.Analyzers\FastCloner.SourceGenerator.Analyzers.csproj" />
19+
</ItemGroup>
20+
</Project>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using FastCloner.SourceGenerator.Analyzers;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using Microsoft.CodeAnalysis.Text;
13+
using Document = Microsoft.CodeAnalysis.Document;
14+
15+
namespace FastCloner.SourceGenerator.CodeFixes;
16+
17+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FastClonerClonableCodeFixProvider)), Shared]
18+
public class FastClonerClonableCodeFixProvider : CodeFixProvider
19+
{
20+
public sealed override ImmutableArray<string> FixableDiagnosticIds => [FastClonerClonableAnalyzer.DiagnosticId];
21+
22+
public sealed override FixAllProvider GetFixAllProvider()
23+
{
24+
return WellKnownFixAllProviders.BatchFixer;
25+
}
26+
27+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
28+
{
29+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
30+
Diagnostic diagnostic = context.Diagnostics.First();
31+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
32+
ClassDeclarationSyntax? declaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().First();
33+
34+
if (declaration is not null)
35+
{
36+
context.RegisterCodeFix(
37+
CodeAction.Create(
38+
title: "Make class partial",
39+
createChangedDocument: c => MakeClassPartialAsync(context.Document, declaration, c),
40+
equivalenceKey: "Make class partial"),
41+
diagnostic);
42+
}
43+
}
44+
45+
private static async Task<Document> MakeClassPartialAsync(Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
46+
{
47+
SyntaxToken partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword);
48+
SyntaxTokenList newModifiers = classDeclaration.Modifiers.Add(partialToken);
49+
ClassDeclarationSyntax newClassDeclaration = classDeclaration.WithModifiers(newModifiers);
50+
SyntaxNode? oldRoot = await document.GetSyntaxRootAsync(cancellationToken);
51+
SyntaxNode? newRoot = oldRoot?.ReplaceNode(classDeclaration, newClassDeclaration);
52+
return newRoot is not null ? document.WithSyntaxRoot(newRoot) : document;
53+
}
54+
}

FastCloner.SourceGenerator.Console/FastCloner.SourceGenerator.Console.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="..\FastCloner.SourceGenerator\FastCloner.SourceGenerator.csproj"
13+
OutputItemType="Analyzer"
14+
ReferenceOutputAssembly="true" />
15+
16+
<ProjectReference Include="..\FastCloner.SourceGenerator.Analyzers\FastCloner.SourceGenerator.Analyzers.csproj"
17+
OutputItemType="Analyzer"
18+
ReferenceOutputAssembly="false" />
19+
20+
<ProjectReference Include="..\FastCloner.SourceGenerator.CodeFixes\FastCloner.SourceGenerator.CodeFixes.csproj"
1321
OutputItemType="Analyzer"
1422
ReferenceOutputAssembly="false" />
1523
</ItemGroup>

FastCloner.SourceGenerator.Console/Program.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
namespace FastCloner.SourceGenerator.Console;
1+
using FastCloner.SourceGenerator.Shared;
2+
3+
namespace FastCloner.SourceGenerator.Console;
24
using System;
35

4-
class Program
6+
[FastClonerClonable]
7+
public partial class Person
58
{
6-
7-
public class Person
8-
{
9-
public string Name { get; set; }
10-
public int Age { get; set; }
11-
public List<string> Hobbies { get; set; }
12-
}
9+
public string Name { get; set; }
10+
public int Age { get; set; }
11+
public List<string> Hobbies { get; set; }
12+
}
1313

14-
14+
class Program
15+
{
1516
static void Main(string[] args)
1617
{
1718
Person person = new Person
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>preview</LangVersion>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
3+
namespace FastCloner.SourceGenerator.Shared;
4+
5+
[AttributeUsage(AttributeTargets.Class)]
6+
public class FastClonerClonableAttribute : Attribute
7+
{
8+
9+
}

FastCloner.SourceGenerator/FastCloner.SourceGenerator.csproj

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5-
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
6-
<LangVersion>13</LangVersion>
5+
<LangVersion>preview</LangVersion>
76
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
87
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
8+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
9+
910
</PropertyGroup>
1011

1112
<ItemGroup>
12-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
13-
<PrivateAssets>all</PrivateAssets>
14-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15-
</PackageReference>
16-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\FastCloner.SourceGenerator.Analyzers\FastCloner.SourceGenerator.Analyzers.csproj"
18+
PrivateAssets="all"
19+
ReferenceOutputAssembly="false"
20+
Pack="true"
21+
PackagePath="analyzers/dotnet/cs" />
22+
23+
<ProjectReference Include="..\FastCloner.SourceGenerator.CodeFixes\FastCloner.SourceGenerator.CodeFixes.csproj"
24+
PrivateAssets="all"
25+
ReferenceOutputAssembly="false"
26+
Pack="true"
27+
PackagePath="analyzers/dotnet/cs" />
28+
29+
<ProjectReference Include="..\FastCloner.SourceGenerator.Shared\FastCloner.SourceGenerator.Shared.csproj" />
1730
</ItemGroup>
1831

1932
</Project>

0 commit comments

Comments
 (0)