1+ // Copyright (c) .NET Foundation and Contributors
2+ //
3+ // Licensed under the Apache License, Version 2.0 (the "License");
4+ // you may not use this file except in compliance with the License.
5+ // You may obtain a copy of the License at
6+ //
7+ // http://www.apache.org/licenses/LICENSE-2.0
8+ //
9+ // Unless required by applicable law or agreed to in writing, software
10+ // distributed under the License is distributed on an "AS IS" BASIS,
11+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ // See the License for the specific language governing permissions and
13+ // limitations under the License.
14+
15+ namespace SourceGenerator ;
16+
17+ [ Generator ]
18+ public class InheritedCloneGenerator : ISourceGenerator {
19+ const string AttributeName = "GenerateClone" ;
20+
21+ public void Initialize ( GeneratorInitializationContext context ) { }
22+
23+ public void Execute ( GeneratorExecutionContext context ) {
24+ var compilation = context . Compilation ;
25+
26+ var candidates = compilation . FindAnnotatedClass ( AttributeName , false ) ;
27+
28+ foreach ( var candidate in candidates ) {
29+ var semanticModel = compilation . GetSemanticModel ( candidate . SyntaxTree ) ;
30+ var genericClassSymbol = semanticModel . GetDeclaredSymbol ( candidate ) ;
31+ if ( genericClassSymbol == null ) continue ;
32+
33+ // Get the method name from the attribute Name argument
34+ var attributeData = genericClassSymbol . GetAttributes ( ) . FirstOrDefault ( a => a . AttributeClass ? . Name == $ "{ AttributeName } Attribute") ;
35+ var methodName = ( string ) attributeData . NamedArguments . FirstOrDefault ( arg => arg . Key == "Name" ) . Value . Value ;
36+
37+ // Get the generic argument type where properties need to be copied from
38+ var attributeSyntax = candidate . AttributeLists
39+ . SelectMany ( l => l . Attributes )
40+ . FirstOrDefault ( a => a . Name . ToString ( ) . StartsWith ( AttributeName ) ) ;
41+ if ( attributeSyntax == null ) continue ; // This should never happen
42+
43+ var typeArgumentSyntax = ( ( GenericNameSyntax ) attributeSyntax . Name ) . TypeArgumentList . Arguments [ 0 ] ;
44+ var typeSymbol = ( INamedTypeSymbol ) semanticModel . GetSymbolInfo ( typeArgumentSyntax ) . Symbol ;
45+
46+ var code = GenerateMethod ( candidate , genericClassSymbol , typeSymbol , methodName ) ;
47+ context . AddSource ( $ "{ genericClassSymbol . Name } .Clone.g.cs", SourceText . From ( code , Encoding . UTF8 ) ) ;
48+ }
49+ }
50+
51+ static string GenerateMethod (
52+ TypeDeclarationSyntax classToExtendSyntax ,
53+ INamedTypeSymbol classToExtendSymbol ,
54+ INamedTypeSymbol classToClone ,
55+ string methodName
56+ ) {
57+ var namespaceName = classToExtendSymbol . ContainingNamespace . ToDisplayString ( ) ;
58+ var className = classToExtendSyntax . Identifier . Text ;
59+ var genericTypeParameters = string . Join ( ", " , classToExtendSymbol . TypeParameters . Select ( tp => tp . Name ) ) ;
60+ var classDeclaration = classToExtendSymbol . TypeParameters . Length > 0 ? $ "{ className } <{ genericTypeParameters } >" : className ;
61+
62+ var all = classToClone . GetBaseTypesAndThis ( ) ;
63+ var props = all . SelectMany ( x => x . GetMembers ( ) . OfType < IPropertySymbol > ( ) ) . ToArray ( ) ;
64+ var usings = classToExtendSyntax . SyntaxTree . GetCompilationUnitRoot ( ) . Usings . Select ( u => u . ToString ( ) ) ;
65+
66+ var constructorParams = classToExtendSymbol . Constructors . First ( ) . Parameters . ToArray ( ) ;
67+ var constructorArgs = string . Join ( ", " , constructorParams . Select ( p => $ "original.{ GetPropertyName ( p . Name , props ) } ") ) ;
68+ var constructorParamNames = constructorParams . Select ( p => p . Name ) . ToArray ( ) ;
69+
70+ var properties = props
71+ // ReSharper disable once PossibleUnintendedLinearSearchInSet
72+ . Where ( prop => ! constructorParamNames . Contains ( prop . Name , StringComparer . OrdinalIgnoreCase ) && prop . SetMethod != null )
73+ . Select ( prop => $ " { prop . Name } = original.{ prop . Name } ,")
74+ . ToArray ( ) ;
75+
76+ const string template = """
77+ {Usings}
78+
79+ namespace {Namespace};
80+
81+ public partial class {ClassDeclaration} {
82+ public static {ClassDeclaration} {MethodName}({OriginalClassName} original)
83+ => new {ClassDeclaration}({ConstructorArgs}) {
84+ {Properties}
85+ };
86+ }
87+ """ ;
88+
89+ var code = template
90+ . Replace ( "{Usings}" , string . Join ( "\n " , usings ) )
91+ . Replace ( "{Namespace}" , namespaceName )
92+ . Replace ( "{ClassDeclaration}" , classDeclaration )
93+ . Replace ( "{OriginalClassName}" , classToClone . Name )
94+ . Replace ( "{MethodName}" , methodName )
95+ . Replace ( "{ConstructorArgs}" , constructorArgs )
96+ . Replace ( "{Properties}" , string . Join ( "\n " , properties ) . TrimEnd ( ',' ) ) ;
97+
98+ return code ;
99+
100+ static string GetPropertyName ( string parameterName , IPropertySymbol [ ] properties ) {
101+ var property = properties . FirstOrDefault ( p => string . Equals ( p . Name , parameterName , StringComparison . OrdinalIgnoreCase ) ) ;
102+ return property ? . Name ?? parameterName ;
103+ }
104+ }
105+ }
0 commit comments