Dreamine.MVVM.Generators 1.0.13
Dreamine.MVVM.Generators 프로젝트의 API와 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
DreamineAutoWiringGenerator.cs
이 파일의 문서화 페이지로 가기
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp.Syntax;
3using Microsoft.CodeAnalysis.Text;
4using System;
5using System.Collections.Generic;
6using System.Collections.Immutable;
7using System.Linq;
8using System.Text;
9
11{
17 [Generator]
18 public sealed class DreamineAutoWiringGenerator : IIncrementalGenerator
19 {
20 private const string ModelAttributeMetadataName = "Dreamine.MVVM.Attributes.DreamineModelAttribute";
21 private const string EventAttributeMetadataName = "Dreamine.MVVM.Attributes.DreamineEventAttribute";
22 private const string PropertyAttributeMetadataName = "Dreamine.MVVM.Attributes.DreaminePropertyAttribute";
23
28 public void Initialize(IncrementalGeneratorInitializationContext context)
29 {
30 IncrementalValueProvider<AttributeSymbolSet> attributeSymbolsProvider =
31 context.CompilationProvider.Select(
32 static (compilation, _) => new AttributeSymbolSet(
33 compilation.GetTypeByMetadataName(ModelAttributeMetadataName),
34 compilation.GetTypeByMetadataName(EventAttributeMetadataName),
35 compilation.GetTypeByMetadataName(PropertyAttributeMetadataName)));
36
37 IncrementalValueProvider<ImmutableArray<AutoWiringCandidate>> candidatesProvider =
38 context.SyntaxProvider
39 .CreateSyntaxProvider(
40 predicate: static (node, _) => IsCandidateSyntax(node),
41 transform: static (syntaxContext, _) => syntaxContext)
42 .Combine(attributeSymbolsProvider)
43 .Select(static (pair, _) => TryCreateCandidate(pair.Left, pair.Right))
44 .Where(static candidate => candidate is not null)
45 .Select(static (candidate, _) => candidate!)
46 .Collect();
47
48 context.RegisterSourceOutput(candidatesProvider, static (sourceProductionContext, candidates) =>
49 {
50 Emit(sourceProductionContext, candidates);
51 });
52 }
53
59 private static bool IsCandidateSyntax(SyntaxNode node)
60 {
61 if (node is not VariableDeclaratorSyntax variable)
62 {
63 return false;
64 }
65
66 if (variable.Parent is not VariableDeclarationSyntax variableDeclaration)
67 {
68 return false;
69 }
70
71 if (variableDeclaration.Parent is not FieldDeclarationSyntax fieldDeclaration)
72 {
73 return false;
74 }
75
76 return fieldDeclaration.AttributeLists.Count > 0;
77 }
78
86 GeneratorSyntaxContext context,
87 AttributeSymbolSet attributeSymbols)
88 {
89 if (attributeSymbols.IsIncomplete)
90 {
91 return null;
92 }
93
94 if (context.Node is not VariableDeclaratorSyntax variable)
95 {
96 return null;
97 }
98
99 if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol)
100 {
101 return null;
102 }
103
104 if (fieldSymbol.ContainingType is null)
105 {
106 return null;
107 }
108
109 // 1차 버전에서는 중첩 타입은 제외
110 if (fieldSymbol.ContainingType.ContainingType is not null)
111 {
112 return null;
113 }
114
115 AttributeData? matchedAttribute;
116 CandidateKind? kind = GetCandidateKind(fieldSymbol, attributeSymbols, out matchedAttribute);
117 if (kind is null || matchedAttribute is null)
118 {
119 return null;
120 }
121
122 string? configuredPropertyName = GetConfiguredPropertyName(matchedAttribute);
123 string? generatedPropertyName = ResolveGeneratedPropertyName(fieldSymbol.Name, configuredPropertyName);
124
125 if (string.IsNullOrWhiteSpace(generatedPropertyName))
126 {
127 return null;
128 }
129
130 return new AutoWiringCandidate(
131 fieldSymbol,
132 fieldSymbol.ContainingType,
133 kind.Value,
134 generatedPropertyName!);
135 }
136
142 private static void Emit(
143 SourceProductionContext context,
144 ImmutableArray<AutoWiringCandidate> candidates)
145 {
146 if (candidates.IsDefaultOrEmpty)
147 {
148 return;
149 }
150
151 foreach (AutoWiringCandidate candidate in candidates)
152 {
154 {
155 continue;
156 }
157
158 string source = BuildSource(candidate);
159 string fileName = BuildFileName(candidate);
160
161 context.AddSource(fileName, SourceText.From(source, Encoding.UTF8));
162 }
163 }
164
173 IFieldSymbol fieldSymbol,
174 AttributeSymbolSet attributeSymbols,
175 out AttributeData? matchedAttribute)
176 {
177 foreach (AttributeData attribute in fieldSymbol.GetAttributes())
178 {
179 INamedTypeSymbol? attributeClass = attribute.AttributeClass;
180 if (attributeClass is null)
181 {
182 continue;
183 }
184
185 if (SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbols.ModelAttribute))
186 {
187 matchedAttribute = attribute;
188 return CandidateKind.Model;
189 }
190
191 if (SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbols.EventAttribute))
192 {
193 matchedAttribute = attribute;
194 return CandidateKind.Event;
195 }
196
197 if (SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbols.PropertyAttribute))
198 {
199 matchedAttribute = attribute;
200 return CandidateKind.Property;
201 }
202 }
203
204 matchedAttribute = null;
205 return null;
206 }
207
213 private static string? GetConfiguredPropertyName(AttributeData attribute)
214 {
215 foreach (KeyValuePair<string, TypedConstant> namedArgument in attribute.NamedArguments)
216 {
217 if (namedArgument.Key == "PropertyName" &&
218 namedArgument.Value.Value is string namedValue &&
219 !string.IsNullOrWhiteSpace(namedValue))
220 {
221 return namedValue;
222 }
223 }
224
225 if (attribute.ConstructorArguments.Length > 0 &&
226 attribute.ConstructorArguments[0].Value is string ctorValue &&
227 !string.IsNullOrWhiteSpace(ctorValue))
228 {
229 return ctorValue;
230 }
231
232 return null;
233 }
234
241 private static string? ResolveGeneratedPropertyName(string fieldName, string? configuredPropertyName)
242 {
243 if (!string.IsNullOrWhiteSpace(configuredPropertyName))
244 {
245 return configuredPropertyName;
246 }
247
248 string normalized = fieldName.TrimStart('_');
249 if (string.IsNullOrWhiteSpace(normalized))
250 {
251 return null;
252 }
253
254 if (normalized.Length == 1)
255 {
256 return normalized.ToUpperInvariant();
257 }
258
259 return char.ToUpperInvariant(normalized[0]) + normalized.Substring(1);
260 }
261
268 private static bool HasConflictingMember(INamedTypeSymbol typeSymbol, string memberName)
269 {
270 return typeSymbol.GetMembers(memberName).Length > 0;
271 }
272
278 private static string BuildFileName(AutoWiringCandidate candidate)
279 {
280 string namespaceName = candidate.ContainingType.ContainingNamespace.IsGlobalNamespace
281 ? "Global"
282 : Sanitize(candidate.ContainingType.ContainingNamespace.ToDisplayString());
283
284 return namespaceName + "_" +
285 candidate.ContainingType.Name + "_" +
286 candidate.GeneratedPropertyName + "_AutoWiring.g.cs";
287 }
288
294 private static string BuildSource(AutoWiringCandidate candidate)
295 {
296 string namespaceName = candidate.ContainingType.ContainingNamespace.IsGlobalNamespace
297 ? string.Empty
298 : candidate.ContainingType.ContainingNamespace.ToDisplayString();
299
300 string className = candidate.ContainingType.Name;
301 string typeName = candidate.FieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
302 string fieldName = candidate.FieldSymbol.Name;
303 string propertyName = candidate.GeneratedPropertyName;
304
305 StringBuilder builder = new StringBuilder();
306 builder.AppendLine("// <auto-generated />");
307 builder.AppendLine("#nullable enable");
308 builder.AppendLine("using Dreamine.MVVM.Core;");
309
310 if (!string.IsNullOrWhiteSpace(namespaceName))
311 {
312 builder.AppendLine("namespace " + namespaceName);
313 builder.AppendLine("{");
314 }
315
316 builder.AppendLine(" public partial class " + className);
317 builder.AppendLine(" {");
318
319 AppendPropertyCode(builder, candidate.Kind, candidate.FieldSymbol, typeName, fieldName, propertyName);
320
321 builder.AppendLine(" }");
322
323 if (!string.IsNullOrWhiteSpace(namespaceName))
324 {
325 builder.AppendLine("}");
326 }
327
328 return builder.ToString();
329 }
330
340 private static void AppendPropertyCode(
341 StringBuilder builder,
342 CandidateKind kind,
343 IFieldSymbol fieldSymbol,
344 string typeName,
345 string fieldName,
346 string propertyName)
347 {
348 switch (kind)
349 {
350 case CandidateKind.Property:
351 builder.AppendLine(" /// <summary>");
352 builder.AppendLine(" /// 생성된 프로퍼티입니다.");
353 builder.AppendLine(" /// </summary>");
354 builder.AppendLine(" public " + typeName + " " + propertyName);
355 builder.AppendLine(" {");
356 builder.AppendLine(" get => " + fieldName + ";");
357 builder.AppendLine(" set => SetProperty(ref " + fieldName + ", value);");
358 builder.AppendLine(" }");
359 break;
360
361 case CandidateKind.Model:
363 builder,
364 fieldSymbol,
365 typeName,
366 fieldName,
367 propertyName,
368 "new " + typeName + "()");
369 break;
370
371 case CandidateKind.Event:
373 builder,
374 fieldSymbol,
375 typeName,
376 fieldName,
377 propertyName,
378 "DMContainer.Resolve<" + typeName + ">()");
379 break;
380 }
381 }
382
392 private static void AppendLazyAccessPropertyCode(
393 StringBuilder builder,
394 IFieldSymbol fieldSymbol,
395 string typeName,
396 string fieldName,
397 string propertyName,
398 string initializerExpression)
399 {
400 builder.AppendLine(" /// <summary>");
401 builder.AppendLine(" /// 생성된 참조 프로퍼티입니다.");
402 builder.AppendLine(" /// </summary>");
403
404 if (fieldSymbol.IsReadOnly || fieldSymbol.Type.IsValueType)
405 {
406 builder.AppendLine(" public " + typeName + " " + propertyName + " => " + fieldName + ";");
407 return;
408 }
409
410 builder.AppendLine(" public " + typeName + " " + propertyName);
411 builder.AppendLine(" {");
412 builder.AppendLine(" get");
413 builder.AppendLine(" {");
414 builder.AppendLine(" if (" + fieldName + " is null)");
415 builder.AppendLine(" {");
416 builder.AppendLine(" " + fieldName + " = " + initializerExpression + ";");
417 builder.AppendLine(" }");
418 builder.AppendLine();
419 builder.AppendLine(" return " + fieldName + ";");
420 builder.AppendLine(" }");
421 builder.AppendLine(" }");
422 }
423
429 private static string Sanitize(string name)
430 {
431 return name.Replace('.', '_').Replace('+', '_');
432 }
433
437 private enum CandidateKind
438 {
442 }
443
447 private sealed class AttributeSymbolSet
448 {
456 INamedTypeSymbol? modelAttribute,
457 INamedTypeSymbol? eventAttribute,
458 INamedTypeSymbol? propertyAttribute)
459 {
460 ModelAttribute = modelAttribute;
461 EventAttribute = eventAttribute;
462 PropertyAttribute = propertyAttribute;
463 }
464
468 public INamedTypeSymbol? ModelAttribute { get; }
469
473 public INamedTypeSymbol? EventAttribute { get; }
474
478 public INamedTypeSymbol? PropertyAttribute { get; }
479
483 public bool IsIncomplete
484 {
485 get
486 {
487 return ModelAttribute is null ||
488 EventAttribute is null ||
489 PropertyAttribute is null;
490 }
491 }
492 }
493
497 private sealed class AutoWiringCandidate
498 {
507 IFieldSymbol fieldSymbol,
508 INamedTypeSymbol containingType,
509 CandidateKind kind,
510 string generatedPropertyName)
511 {
512 FieldSymbol = fieldSymbol;
513 ContainingType = containingType;
514 Kind = kind;
515 GeneratedPropertyName = generatedPropertyName;
516 }
517
521 public IFieldSymbol FieldSymbol { get; }
522
526 public INamedTypeSymbol ContainingType { get; }
527
531 public CandidateKind Kind { get; }
532
536 public string GeneratedPropertyName { get; }
537 }
538 }
539}
DreamineModelAttribute, DreamineEventAttribute, DreaminePropertyAttribute가 적용된 필드를 기반으로 보조 프로퍼티 코드를 생...
static void Emit(SourceProductionContext context, ImmutableArray< AutoWiringCandidate > candidates)
수집된 후보를 기반으로 소스를 생성합니다.
static string BuildSource(AutoWiringCandidate candidate)
생성 코드를 만듭니다.
static bool IsCandidateSyntax(SyntaxNode node)
후보가 될 수 있는 구문인지 확인합니다.
static void AppendPropertyCode(StringBuilder builder, CandidateKind kind, IFieldSymbol fieldSymbol, string typeName, string fieldName, string propertyName)
종류에 맞는 프로퍼티 코드를 생성합니다.
void Initialize(IncrementalGeneratorInitializationContext context)
증분 생성기 파이프라인을 초기화합니다.
static string BuildFileName(AutoWiringCandidate candidate)
생성 파일 이름을 만듭니다.
static bool HasConflictingMember(INamedTypeSymbol typeSymbol, string memberName)
이미 같은 이름의 멤버가 존재하는지 확인합니다.
static ? AutoWiringCandidate TryCreateCandidate(GeneratorSyntaxContext context, AttributeSymbolSet attributeSymbols)
구문/시맨틱 정보를 바탕으로 생성 대상 후보를 만듭니다.
static string Sanitize(string name)
파일 이름에 안전한 문자열로 변환합니다.
static void AppendLazyAccessPropertyCode(StringBuilder builder, IFieldSymbol fieldSymbol, string typeName, string fieldName, string propertyName, string initializerExpression)
지연 초기화 기반 읽기 전용 프로퍼티 코드를 생성합니다.
static ? string ResolveGeneratedPropertyName(string fieldName, string? configuredPropertyName)
생성할 프로퍼티 이름을 결정합니다.
static ? CandidateKind GetCandidateKind(IFieldSymbol fieldSymbol, AttributeSymbolSet attributeSymbols, out AttributeData? matchedAttribute)
필드에 적용된 Attribute 종류를 판별합니다.
static ? string GetConfiguredPropertyName(AttributeData attribute)
Attribute에 지정된 명시적 프로퍼티 이름을 가져옵니다.
INamedTypeSymbol? ModelAttribute
Model Attribute 심볼을 가져옵니다.
INamedTypeSymbol? EventAttribute
Event Attribute 심볼을 가져옵니다.
AttributeSymbolSet(INamedTypeSymbol? modelAttribute, INamedTypeSymbol? eventAttribute, INamedTypeSymbol? propertyAttribute)
AttributeSymbolSet 클래스의 새 인스턴스를 초기화합니다.
INamedTypeSymbol? PropertyAttribute
Property Attribute 심볼을 가져옵니다.
bool IsIncomplete
필수 심볼이 모두 준비되었는지 여부를 가져옵니다.
자동 생성 대상 필드 메타데이터를 나타냅니다.
INamedTypeSymbol ContainingType
필드를 포함하는 타입을 가져옵니다.
string GeneratedPropertyName
생성할 프로퍼티 이름을 가져옵니다.
AutoWiringCandidate(IFieldSymbol fieldSymbol, INamedTypeSymbol containingType, CandidateKind kind, string generatedPropertyName)
AutoWiringCandidate 클래스의 새 인스턴스를 초기화합니다.