Dreamine.MVVM.Generators 1.0.13
Dreamine.MVVM.Generators 프로젝트의 API와 구성 요소를 제공합니다.
로딩중...
검색중...
일치하는것 없음
DreamineCommandSourceGenerator.cs
이 파일의 문서화 페이지로 가기
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp;
3using Microsoft.CodeAnalysis.CSharp.Syntax;
4using Microsoft.CodeAnalysis.Text;
5using System;
6using System.Collections.Generic;
7using System.Collections.Immutable;
8using System.Linq;
9using System.Text;
10
12{
17 [Generator]
18 public sealed class DreamineCommandSourceGenerator : IIncrementalGenerator
19 {
20 private const string DreamineCommandAttributeMetadataName = "Dreamine.MVVM.Attributes.DreamineCommandAttribute";
21
22 private static readonly DiagnosticDescriptor MethodMustBePartialDescriptor = new(
23 id: "DMCMD001",
24 title: "DreamineCommand method must be partial",
25 messageFormat: "Method '{0}' marked with forwarding [DreamineCommand] must be declared partial",
26 category: "Dreamine.MVVM.Generators",
27 defaultSeverity: DiagnosticSeverity.Error,
28 isEnabledByDefault: true);
29
30 private static readonly DiagnosticDescriptor ContainingTypeMustBePartialDescriptor = new(
31 id: "DMCMD002",
32 title: "Containing type must be partial",
33 messageFormat: "Containing type '{0}' for [DreamineCommand] method must be declared partial",
34 category: "Dreamine.MVVM.Generators",
35 defaultSeverity: DiagnosticSeverity.Error,
36 isEnabledByDefault: true);
37
38 private static readonly DiagnosticDescriptor MethodMustBeParameterlessVoidDescriptor = new(
39 id: "DMCMD003",
40 title: "DreamineCommand method must be parameterless void",
41 messageFormat: "Method '{0}' marked with [DreamineCommand] must be a parameterless void method",
42 category: "Dreamine.MVVM.Generators",
43 defaultSeverity: DiagnosticSeverity.Error,
44 isEnabledByDefault: true);
45
46 private static readonly DiagnosticDescriptor CommandNameConflictDescriptor = new(
47 id: "DMCMD004",
48 title: "Generated command property name conflicts with an existing member",
49 messageFormat: "Generated command property '{0}' conflicts with an existing member in type '{1}'",
50 category: "Dreamine.MVVM.Generators",
51 defaultSeverity: DiagnosticSeverity.Error,
52 isEnabledByDefault: true);
53
54 private static readonly DiagnosticDescriptor CanExecuteMethodNotFoundDescriptor = new(
55 id: "DMCMD005",
56 title: "CanExecute method not found",
57 messageFormat: "CanExecute method '{0}' specified on [DreamineCommand] was not found in type '{1}'. The method must be a parameterless bool method.",
58 category: "Dreamine.MVVM.Generators",
59 defaultSeverity: DiagnosticSeverity.Warning,
60 isEnabledByDefault: true);
61
66 public void Initialize(IncrementalGeneratorInitializationContext context)
67 {
68 IncrementalValueProvider<INamedTypeSymbol?> attributeSymbolProvider =
69 context.CompilationProvider.Select(
70 static (compilation, _) => compilation.GetTypeByMetadataName(DreamineCommandAttributeMetadataName));
71
72 IncrementalValueProvider<ImmutableArray<CommandCandidateModel>> candidateProvider =
73 context.SyntaxProvider
74 .CreateSyntaxProvider(
75 predicate: static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
76 transform: static (syntaxContext, _) => syntaxContext)
77 .Combine(attributeSymbolProvider)
78 .Select(static (pair, _) => TryCreateCandidate(pair.Left, pair.Right))
79 .Where(static candidate => candidate is not null)
80 .Select(static (candidate, _) => candidate!)
81 .Collect();
82
83 context.RegisterSourceOutput(candidateProvider, static (sourceProductionContext, candidates) =>
84 {
85 Emit(sourceProductionContext, candidates);
86 });
87 }
88
96 GeneratorSyntaxContext context,
97 INamedTypeSymbol? attributeSymbol)
98 {
99 if (attributeSymbol is null)
100 {
101 return null;
102 }
103
104 if (context.Node is not MethodDeclarationSyntax methodSyntax)
105 {
106 return null;
107 }
108
109 if (context.SemanticModel.GetDeclaredSymbol(methodSyntax) is not IMethodSymbol methodSymbol)
110 {
111 return null;
112 }
113
114 AttributeData? attributeData = methodSymbol
115 .GetAttributes()
116 .FirstOrDefault(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol));
117
118 if (attributeData is null)
119 {
120 return null;
121 }
122
123 string? targetMethod = GetConstructorStringArgument(attributeData, 0);
124 string? bindTo = GetNamedStringArgument(attributeData, "BindTo");
125 string? commandNameOverride = GetNamedStringArgument(attributeData, "CommandName");
126 string? canExecuteMethod = GetNamedStringArgument(attributeData, "CanExecute");
127
128 string commandPropertyName = BuildCommandPropertyName(methodSymbol.Name, commandNameOverride);
129
130 return new CommandCandidateModel(
131 methodSyntax,
132 methodSymbol,
133 targetMethod,
134 bindTo,
135 commandPropertyName,
136 canExecuteMethod);
137 }
138
144 private static void Emit(
145 SourceProductionContext context,
146 ImmutableArray<CommandCandidateModel> candidates)
147 {
148 if (candidates.IsDefaultOrEmpty)
149 {
150 return;
151 }
152
153 foreach (CommandCandidateModel candidate in candidates)
154 {
155 List<Diagnostic> diagnostics = ValidateCandidate(candidate);
156 foreach (Diagnostic diagnostic in diagnostics)
157 {
158 context.ReportDiagnostic(diagnostic);
159 }
160
161 if (diagnostics.Count > 0)
162 {
163 continue;
164 }
165
166 string source = BuildSource(candidate);
167 string fileName = BuildFileName(candidate);
168
169 context.AddSource(fileName, SourceText.From(source, Encoding.UTF8));
170 }
171 }
172
178 private static List<Diagnostic> ValidateCandidate(CommandCandidateModel candidate)
179 {
180 List<Diagnostic> diagnostics = new List<Diagnostic>();
181
182 bool hasTargetMethod = !string.IsNullOrWhiteSpace(candidate.TargetMethod);
183
184 if (hasTargetMethod && !IsPartialMethod(candidate.MethodSyntax))
185 {
186 diagnostics.Add(Diagnostic.Create(
188 candidate.MethodSyntax.Identifier.GetLocation(),
189 candidate.MethodSymbol.ToDisplayString()));
190 }
191
192 if (!IsContainingTypePartial(candidate.MethodSymbol.ContainingType))
193 {
194 diagnostics.Add(Diagnostic.Create(
196 candidate.MethodSyntax.Identifier.GetLocation(),
197 candidate.MethodSymbol.ContainingType.ToDisplayString()));
198 }
199
201 {
202 diagnostics.Add(Diagnostic.Create(
204 candidate.MethodSyntax.Identifier.GetLocation(),
205 candidate.MethodSymbol.ToDisplayString()));
206 }
207
208 if (HasConflictingMember(candidate.MethodSymbol.ContainingType, candidate.CommandPropertyName))
209 {
210 diagnostics.Add(Diagnostic.Create(
212 candidate.MethodSyntax.Identifier.GetLocation(),
213 candidate.CommandPropertyName,
214 candidate.MethodSymbol.ContainingType.ToDisplayString()));
215 }
216
217 if (!string.IsNullOrWhiteSpace(candidate.CanExecuteMethod) &&
218 !HasValidCanExecuteMethod(candidate.MethodSymbol.ContainingType, candidate.CanExecuteMethod!))
219 {
220 diagnostics.Add(Diagnostic.Create(
222 candidate.MethodSyntax.Identifier.GetLocation(),
223 candidate.CanExecuteMethod,
224 candidate.MethodSymbol.ContainingType.ToDisplayString()));
225 }
226
227 return diagnostics;
228 }
229
235 private static bool IsPartialMethod(MethodDeclarationSyntax methodSyntax)
236 {
237 return methodSyntax.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword));
238 }
239
245 private static bool IsContainingTypePartial(INamedTypeSymbol typeSymbol)
246 {
247 foreach (SyntaxReference syntaxReference in typeSymbol.DeclaringSyntaxReferences)
248 {
249 if (syntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration &&
250 typeDeclaration.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)))
251 {
252 return true;
253 }
254 }
255
256 return false;
257 }
258
264 private static bool IsParameterlessVoidMethod(IMethodSymbol methodSymbol)
265 {
266 return methodSymbol.Parameters.Length == 0 &&
267 methodSymbol.ReturnsVoid &&
268 !methodSymbol.IsGenericMethod;
269 }
270
277 private static bool HasConflictingMember(INamedTypeSymbol typeSymbol, string memberName)
278 {
279 return typeSymbol.GetMembers(memberName).Any();
280 }
281
282 private static bool HasValidCanExecuteMethod(INamedTypeSymbol typeSymbol, string methodName)
283 {
284 return typeSymbol.GetMembers(methodName)
285 .OfType<IMethodSymbol>()
286 .Any(m => m.Parameters.Length == 0 &&
287 !m.ReturnsVoid &&
288 m.ReturnType.SpecialType == SpecialType.System_Boolean);
289 }
290
297 private static string BuildCommandPropertyName(string methodName, string? commandNameOverride)
298 {
299 if (!string.IsNullOrWhiteSpace(commandNameOverride))
300 {
301 return commandNameOverride!;
302 }
303
304 return methodName + "Command";
305 }
306
312 private static string BuildFileName(CommandCandidateModel candidate)
313 {
314 string namespaceName = candidate.MethodSymbol.ContainingNamespace.IsGlobalNamespace
315 ? "Global"
316 : Sanitize(candidate.MethodSymbol.ContainingNamespace.ToDisplayString());
317
318 string typeName = string.Join("_", GetContainingTypeChain(candidate.MethodSymbol.ContainingType).Select(type => type.Name));
319 string methodName = candidate.MethodSymbol.Name;
320
321 return namespaceName + "_" + typeName + "_" + methodName + "_DreamineCommand.g.cs";
322 }
323
329 private static string BuildSource(CommandCandidateModel candidate)
330 {
331 string namespaceName = candidate.MethodSymbol.ContainingNamespace.IsGlobalNamespace
332 ? string.Empty
333 : candidate.MethodSymbol.ContainingNamespace.ToDisplayString();
334
335 List<INamedTypeSymbol> typeChain = GetContainingTypeChain(candidate.MethodSymbol.ContainingType);
336 string commandFieldName = "_" + ToCamel(candidate.CommandPropertyName);
337 string helperTypeName = "__DreamineGeneratedCommand_" + candidate.MethodSymbol.Name;
338 bool hasTargetMethod = !string.IsNullOrWhiteSpace(candidate.TargetMethod);
339 string? normalizedInvocation = hasTargetMethod
341 : null;
342
343 StringBuilder builder = new StringBuilder();
344 builder.AppendLine("// <auto-generated />");
345 builder.AppendLine("#nullable enable");
346 builder.AppendLine("using System;");
347 builder.AppendLine("using System.Windows.Input;");
348 builder.AppendLine();
349
350 if (!string.IsNullOrWhiteSpace(namespaceName))
351 {
352 builder.AppendLine("namespace " + namespaceName);
353 builder.AppendLine("{");
354 }
355
356 for (int i = 0; i < typeChain.Count; i++)
357 {
358 string indent = new string(' ', 4 * (i + 1));
359 builder.AppendLine(indent + GetTypeDeclarationHeader(typeChain[i]));
360 builder.AppendLine(indent + "{");
361 }
362
363 string memberIndent = new string(' ', 4 * (typeChain.Count + 1));
364
365 builder.AppendLine(memberIndent + "/// <summary>");
366 builder.AppendLine(memberIndent + "/// 생성된 ICommand 프로퍼티입니다.");
367 builder.AppendLine(memberIndent + "/// </summary>");
368 builder.AppendLine(memberIndent + "private ICommand? " + commandFieldName + ";");
369 string ctorArgs = !string.IsNullOrWhiteSpace(candidate.CanExecuteMethod)
370 ? candidate.MethodSymbol.Name + ", " + candidate.CanExecuteMethod
371 : candidate.MethodSymbol.Name;
372 builder.AppendLine(memberIndent + "public ICommand " + candidate.CommandPropertyName + " => " + commandFieldName + " ??= new " + helperTypeName + "(" + ctorArgs + ");");
373 builder.AppendLine();
374
375 bool hasBody = candidate.MethodSyntax.Body is not null || candidate.MethodSyntax.ExpressionBody is not null;
376 if (hasTargetMethod && !hasBody)
377 {
378 builder.AppendLine(memberIndent + GetMethodSignatureWithoutAttributes(candidate.MethodSyntax));
379 builder.AppendLine(memberIndent + "{");
380
381 if (!string.IsNullOrWhiteSpace(candidate.BindTo))
382 {
383 builder.AppendLine(memberIndent + " var __result = " + normalizedInvocation + ";");
384 builder.AppendLine(memberIndent + " " + candidate.BindTo + " = __result;");
385 }
386 else
387 {
388 builder.AppendLine(memberIndent + " " + normalizedInvocation + ";");
389 }
390
391 builder.AppendLine(memberIndent + "}");
392 builder.AppendLine();
393 }
394
395 bool hasCanExecute = !string.IsNullOrWhiteSpace(candidate.CanExecuteMethod);
396
397 builder.AppendLine(memberIndent + "private sealed class " + helperTypeName + " : ICommand");
398 builder.AppendLine(memberIndent + "{");
399 builder.AppendLine(memberIndent + " private readonly Action _execute;");
400 if (hasCanExecute)
401 {
402 builder.AppendLine(memberIndent + " private readonly Func<bool> _canExecute;");
403 builder.AppendLine(memberIndent + " public " + helperTypeName + "(Action execute, Func<bool> canExecute)");
404 builder.AppendLine(memberIndent + " {");
405 builder.AppendLine(memberIndent + " _execute = execute ?? throw new ArgumentNullException(nameof(execute));");
406 builder.AppendLine(memberIndent + " _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));");
407 builder.AppendLine(memberIndent + " }");
408 }
409 else
410 {
411 builder.AppendLine(memberIndent + " public " + helperTypeName + "(Action execute)");
412 builder.AppendLine(memberIndent + " {");
413 builder.AppendLine(memberIndent + " _execute = execute ?? throw new ArgumentNullException(nameof(execute));");
414 builder.AppendLine(memberIndent + " }");
415 }
416
417 builder.AppendLine(memberIndent + " public event EventHandler? CanExecuteChanged;");
418 builder.AppendLine(memberIndent + " public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);");
419 if (hasCanExecute)
420 {
421 builder.AppendLine(memberIndent + " public bool CanExecute(object? parameter) => _canExecute();");
422 }
423 else
424 {
425 builder.AppendLine(memberIndent + " public bool CanExecute(object? parameter) => true;");
426 }
427
428 builder.AppendLine(memberIndent + " public void Execute(object? parameter) => _execute();");
429 builder.AppendLine(memberIndent + "}");
430 builder.AppendLine();
431
432 for (int i = typeChain.Count - 1; i >= 0; i--)
433 {
434 string indent = new string(' ', 4 * (i + 1));
435 builder.AppendLine(indent + "}");
436 }
437
438 if (!string.IsNullOrWhiteSpace(namespaceName))
439 {
440 builder.AppendLine("}");
441 }
442
443 return builder.ToString();
444 }
445
451 private static string GetMethodSignatureWithoutAttributes(MethodDeclarationSyntax methodSyntax)
452 {
453 MethodDeclarationSyntax withoutBody = methodSyntax
454 .WithAttributeLists(default)
455 .WithBody(null)
456 .WithExpressionBody(null)
457 .WithSemicolonToken(default);
458
459 return withoutBody.NormalizeWhitespace().ToFullString();
460 }
461
468 private static string? GetConstructorStringArgument(AttributeData attribute, int index)
469 {
470 if (attribute.ConstructorArguments.Length <= index)
471 {
472 return null;
473 }
474
475 return attribute.ConstructorArguments[index].Value?.ToString();
476 }
477
484 private static string? GetNamedStringArgument(AttributeData attribute, string name)
485 {
486 return attribute.NamedArguments
487 .Where(argument => string.Equals(argument.Key, name, StringComparison.Ordinal))
488 .Select(argument => argument.Value.Value?.ToString())
489 .FirstOrDefault();
490 }
491
497 private static string NormalizeInvocation(string targetMethod)
498 {
499 string trimmed = targetMethod.Trim();
500
501 if (trimmed.EndsWith(")", StringComparison.Ordinal))
502 {
503 return trimmed;
504 }
505
506 return trimmed + "()";
507 }
508
514 private static List<INamedTypeSymbol> GetContainingTypeChain(INamedTypeSymbol innerMostType)
515 {
516 Stack<INamedTypeSymbol> stack = new Stack<INamedTypeSymbol>();
517 INamedTypeSymbol? current = innerMostType;
518
519 while (current is not null)
520 {
521 stack.Push(current);
522 current = current.ContainingType;
523 }
524
525 return stack.ToList();
526 }
527
533 private static string GetTypeDeclarationHeader(INamedTypeSymbol typeSymbol)
534 {
535 string kind = typeSymbol.TypeKind switch
536 {
537 TypeKind.Class => "partial class",
538 TypeKind.Struct => "partial struct",
539 _ => "partial class"
540 };
541
542 string accessibility = typeSymbol.DeclaredAccessibility switch
543 {
544 Accessibility.Public => "public",
545 Accessibility.Internal => "internal",
546 Accessibility.Private => "private",
547 Accessibility.Protected => "protected",
548 Accessibility.ProtectedAndInternal => "protected internal",
549 Accessibility.ProtectedOrInternal => "protected internal",
550 _ => "internal"
551 };
552
553 string staticKeyword = typeSymbol.IsStatic ? " static" : string.Empty;
554
555 string typeParameters = typeSymbol.TypeParameters.Length > 0
556 ? "<" + string.Join(", ", typeSymbol.TypeParameters.Select(parameter => parameter.Name)) + ">"
557 : string.Empty;
558
559 return accessibility + staticKeyword + " " + kind + " " + typeSymbol.Name + typeParameters;
560 }
561
567 private static string Sanitize(string name)
568 {
569 return name.Replace('.', '_').Replace('+', '_');
570 }
571
577 private static string ToCamel(string name)
578 {
579 if (string.IsNullOrEmpty(name))
580 {
581 return name;
582 }
583
584 if (name.Length == 1)
585 {
586 return name.ToLowerInvariant();
587 }
588
589 return char.ToLowerInvariant(name[0]) + name.Substring(1);
590 }
591
595 private sealed class CommandCandidateModel
596 {
607 MethodDeclarationSyntax methodSyntax,
608 IMethodSymbol methodSymbol,
609 string? targetMethod,
610 string? bindTo,
611 string commandPropertyName,
612 string? canExecuteMethod = null)
613 {
614 MethodSyntax = methodSyntax;
615 MethodSymbol = methodSymbol;
616 TargetMethod = targetMethod;
617 BindTo = bindTo;
618 CommandPropertyName = commandPropertyName;
619 CanExecuteMethod = canExecuteMethod;
620 }
621
625 public MethodDeclarationSyntax MethodSyntax { get; }
626
630 public IMethodSymbol MethodSymbol { get; }
631
635 public string? TargetMethod { get; }
636
640 public string? BindTo { get; }
641
645 public string CommandPropertyName { get; }
646
650 public string? CanExecuteMethod { get; }
651 }
652 }
653}
DreamineCommandAttribute가 적용된 메서드를 기반으로 커맨드 프로퍼티와 forwarding 메서드 구현을 생성하는 증분 생성기입니다.
static string NormalizeInvocation(string targetMethod)
TargetMethod 문자열을 호출 형태로 정규화합니다.
static string GetTypeDeclarationHeader(INamedTypeSymbol typeSymbol)
partial 타입 선언 헤더를 생성합니다.
static void Emit(SourceProductionContext context, ImmutableArray< CommandCandidateModel > candidates)
수집된 후보를 진단하고 소스를 생성합니다.
static bool IsContainingTypePartial(INamedTypeSymbol typeSymbol)
containing type이 partial인지 확인합니다.
static bool HasConflictingMember(INamedTypeSymbol typeSymbol, string memberName)
같은 이름의 멤버가 이미 존재하는지 확인합니다.
static string GetMethodSignatureWithoutAttributes(MethodDeclarationSyntax methodSyntax)
원본 메서드에서 Attribute와 본문을 제거한 시그니처를 만듭니다.
static ? string GetConstructorStringArgument(AttributeData attribute, int index)
생성자 문자열 인자를 가져옵니다.
void Initialize(IncrementalGeneratorInitializationContext context)
증분 생성기 파이프라인을 초기화합니다.
static string Sanitize(string name)
파일명에 안전한 형태로 문자열을 정리합니다.
static string BuildSource(CommandCandidateModel candidate)
생성 코드를 만듭니다.
static bool IsPartialMethod(MethodDeclarationSyntax methodSyntax)
partial 메서드 여부를 확인합니다.
static List< INamedTypeSymbol > GetContainingTypeChain(INamedTypeSymbol innerMostType)
바깥 타입부터 안쪽 타입까지의 체인을 반환합니다.
static string BuildFileName(CommandCandidateModel candidate)
생성 파일 이름을 만듭니다.
static ? CommandCandidateModel TryCreateCandidate(GeneratorSyntaxContext context, INamedTypeSymbol? attributeSymbol)
구문/시맨틱 정보를 기반으로 생성 후보를 구성합니다.
static List< Diagnostic > ValidateCandidate(CommandCandidateModel candidate)
후보의 유효성을 검증합니다.
static bool IsParameterlessVoidMethod(IMethodSymbol methodSymbol)
메서드가 parameterless void 형식인지 확인합니다.
static bool HasValidCanExecuteMethod(INamedTypeSymbol typeSymbol, string methodName)
static string ToCamel(string name)
PascalCase 문자열을 camelCase로 변환합니다.
static string BuildCommandPropertyName(string methodName, string? commandNameOverride)
생성할 커맨드 프로퍼티 이름을 결정합니다.
static ? string GetNamedStringArgument(AttributeData attribute, string name)
named argument 문자열 값을 가져옵니다.
string CommandPropertyName
생성할 커맨드 프로퍼티 이름을 가져옵니다.
string? CanExecuteMethod
CanExecute 판단 메서드 이름을 가져옵니다.
MethodDeclarationSyntax MethodSyntax
원본 메서드 구문을 가져옵니다.
CommandCandidateModel(MethodDeclarationSyntax methodSyntax, IMethodSymbol methodSymbol, string? targetMethod, string? bindTo, string commandPropertyName, string? canExecuteMethod=null)
CommandCandidateModel 클래스의 새 인스턴스를 초기화합니다.
string? BindTo
반환값을 대입할 프로퍼티 이름을 가져옵니다.