Skip to content

Commit 17d4df6

Browse files
committed
Add diagnostics for orphaned [ObservableProperty] dependent attributes
1 parent e694def commit 17d4df6

File tree

7 files changed

+120
-20
lines changed

7 files changed

+120
-20
lines changed

CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ MVVMTK0016 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error |
2424
MVVMTK0017 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2525
MVVMTK0018 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
2626
MVVMTK0019 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
27+
MVVMTK0020 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,19 @@ internal static class Execute
132132
validationAttributes.ToImmutable());
133133
}
134134

135+
/// <summary>
136+
/// Gets the diagnostics for a field with invalid attribute uses.
137+
/// </summary>
138+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
139+
/// <returns>The resulting <see cref="Diagnostic"/> instance for <paramref name="fieldSymbol"/>.</returns>
140+
public static Diagnostic GetDiagnosticForFieldWithOrphanedDependentAttributes(IFieldSymbol fieldSymbol)
141+
{
142+
return FieldWithOrphanedDependentObservablePropertyAttributesError.CreateDiagnostic(
143+
fieldSymbol,
144+
fieldSymbol.ContainingType,
145+
fieldSymbol.Name);
146+
}
147+
135148
/// <summary>
136149
/// Validates the containing type for a given field being annotated.
137150
/// </summary>

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Linq;
88
using System.Text;
99
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
10-
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1110
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1211
using CommunityToolkit.Mvvm.SourceGenerators.Models;
1312
using Microsoft.CodeAnalysis;
@@ -40,6 +39,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4039
fieldSymbols
4140
.Where(static item => item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"));
4241

42+
// Get diagnostics for fields using [AlsoNotifyChangeFor] and [AlsoNotifyCanExecuteFor], but not [ObservableProperty]
43+
IncrementalValuesProvider<Diagnostic> fieldSymbolsWithOrphanedDependentAttributeWithErrors =
44+
fieldSymbols
45+
.Where(static item =>
46+
(item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") ||
47+
item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute")) &&
48+
!item.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"))
49+
.Select(static (item, _) => Execute.GetDiagnosticForFieldWithOrphanedDependentAttributes(item));
50+
51+
// Output the diagnostics
52+
context.ReportDiagnostics(fieldSymbolsWithOrphanedDependentAttributeWithErrors);
53+
4354
// Filter by language version
4455
context.FilterWithLanguageVersion(ref fieldSymbolsWithAttribute, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
4556

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ internal static class DiagnosticDescriptors
276276
/// </summary>
277277
public static readonly DiagnosticDescriptor InvalidAttributeCombinationForINotifyPropertyChangedAttributeError = new DiagnosticDescriptor(
278278
id: "MVVMTK0017",
279-
title: $"Invalid target type for [INotifyPropertyChanged]",
280-
messageFormat: $"Cannot apply [INotifyPropertyChanged] to type {{0}}, as it already has this attribute or [ObservableObject] applied to it (including base types)",
279+
title: "Invalid target type for [INotifyPropertyChanged]",
280+
messageFormat: "Cannot apply [INotifyPropertyChanged] to type {0}, as it already has this attribute or [ObservableObject] applied to it (including base types)",
281281
category: typeof(INotifyPropertyChangedGenerator).FullName,
282282
defaultSeverity: DiagnosticSeverity.Error,
283283
isEnabledByDefault: true,
@@ -292,8 +292,8 @@ internal static class DiagnosticDescriptors
292292
/// </summary>
293293
public static readonly DiagnosticDescriptor InvalidAttributeCombinationForObservableObjectAttributeError = new DiagnosticDescriptor(
294294
id: "MVVMTK0018",
295-
title: $"Invalid target type for [ObservableObject]",
296-
messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already has this attribute or [INotifyPropertyChanged] applied to it (including base types)",
295+
title: "Invalid target type for [ObservableObject]",
296+
messageFormat: "Cannot apply [ObservableObject] to type {0}, as it already has this attribute or [INotifyPropertyChanged] applied to it (including base types)",
297297
category: typeof(ObservableObjectGenerator).FullName,
298298
defaultSeverity: DiagnosticSeverity.Error,
299299
isEnabledByDefault: true,
@@ -308,11 +308,27 @@ internal static class DiagnosticDescriptors
308308
/// </summary>
309309
public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyFieldError = new DiagnosticDescriptor(
310310
id: "MVVMTK0019",
311-
title: $"Invalid containing type for [ObservableProperty] field",
312-
messageFormat: $"The field {{0}}.{{1}} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]",
311+
title: "Invalid containing type for [ObservableProperty] field",
312+
messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]",
313313
category: typeof(ObservablePropertyGenerator).FullName,
314314
defaultSeverity: DiagnosticSeverity.Error,
315315
isEnabledByDefault: true,
316316
description: "Fields annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).",
317317
helpLinkUri: "https://aka.ms/mvvmtoolkit");
318+
319+
/// <summary>
320+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <c>[ObservableProperty]</c> is applied to a field in an invalid type.
321+
/// <para>
322+
/// Format: <c>"The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor] and [AlsoNotifyCanExecuteFor]"</c>.
323+
/// </para>
324+
/// </summary>
325+
public static readonly DiagnosticDescriptor FieldWithOrphanedDependentObservablePropertyAttributesError = new DiagnosticDescriptor(
326+
id: "MVVMTK0020",
327+
title: "Invalid use of attributes dependent on [ObservableProperty]",
328+
messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [AlsoNotifyChangeFor] and [AlsoNotifyCanExecuteFor]",
329+
category: typeof(ObservablePropertyGenerator).FullName,
330+
defaultSeverity: DiagnosticSeverity.Error,
331+
isEnabledByDefault: true,
332+
description: "Fields not annotated with [ObservableProperty] cannot use [AlsoNotifyChangeFor] and [AlsoNotifyCanExecuteFor].",
333+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
318334
}

CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticExtensions.cs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1313

1414
/// <summary>
15-
/// Extension methods for <see cref="GeneratorExecutionContext"/>, specifically for reporting diagnostics.
15+
/// Extension methods specifically for creating diagnostics.
1616
/// </summary>
1717
internal static class DiagnosticExtensions
1818
{
@@ -29,22 +29,18 @@ public static void Add(
2929
ISymbol symbol,
3030
params object[] args)
3131
{
32-
diagnostics.Add(Diagnostic.Create(descriptor, symbol.Locations.FirstOrDefault(), args));
32+
diagnostics.Add(descriptor.CreateDiagnostic(symbol, args));
3333
}
3434

3535
/// <summary>
36-
/// Registers an output node into an <see cref="IncrementalGeneratorInitializationContext"/> to output diagnostics.
36+
/// Creates a new <see cref="Diagnostic"/> instance with the specified parameters.
3737
/// </summary>
38-
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> instance.</param>
39-
/// <param name="diagnostics">The input <see cref="IncrementalValuesProvider{TValues}"/> sequence of diagnostics.</param>
40-
public static void ReportDiagnostics(this IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<ImmutableArray<Diagnostic>> diagnostics)
38+
/// <param name="descriptor">The input <see cref="DiagnosticDescriptor"/> for the diagnostics to create.</param>
39+
/// <param name="symbol">The source <see cref="ISymbol"/> to attach the diagnostics to.</param>
40+
/// <param name="args">The optional arguments for the formatted message to include.</param>
41+
/// <returns>The resulting <see cref="Diagnostic"/> instance.</returns>
42+
public static Diagnostic CreateDiagnostic(this DiagnosticDescriptor descriptor, ISymbol symbol, params object[] args)
4143
{
42-
context.RegisterSourceOutput(diagnostics, static (context, diagnostics) =>
43-
{
44-
foreach (Diagnostic diagnostic in diagnostics)
45-
{
46-
context.ReportDiagnostic(diagnostic);
47-
}
48-
});
44+
return Diagnostic.Create(descriptor, symbol.Locations.FirstOrDefault(), args);
4945
}
5046
}

CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Immutable;
67
using Microsoft.CodeAnalysis;
78
using Microsoft.CodeAnalysis.CSharp;
89

@@ -84,4 +85,30 @@ public static void RegisterConditionalImplementationSourceOutput<T>(
8485
}
8586
});
8687
}
88+
89+
/// <summary>
90+
/// Registers an output node into an <see cref="IncrementalGeneratorInitializationContext"/> to output diagnostics.
91+
/// </summary>
92+
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> instance.</param>
93+
/// <param name="diagnostics">The input <see cref="IncrementalValuesProvider{TValues}"/> sequence of diagnostics.</param>
94+
public static void ReportDiagnostics(this IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<ImmutableArray<Diagnostic>> diagnostics)
95+
{
96+
context.RegisterSourceOutput(diagnostics, static (context, diagnostics) =>
97+
{
98+
foreach (Diagnostic diagnostic in diagnostics)
99+
{
100+
context.ReportDiagnostic(diagnostic);
101+
}
102+
});
103+
}
104+
105+
/// <summary>
106+
/// Registers an output node into an <see cref="IncrementalGeneratorInitializationContext"/> to output diagnostics.
107+
/// </summary>
108+
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> instance.</param>
109+
/// <param name="diagnostics">The input <see cref="IncrementalValuesProvider{TValues}"/> sequence of diagnostics.</param>
110+
public static void ReportDiagnostics(this IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<Diagnostic> diagnostics)
111+
{
112+
context.RegisterSourceOutput(diagnostics, static (context, diagnostic) => context.ReportDiagnostic(diagnostic));
113+
}
87114
}

tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,42 @@ public partial class MyViewModel : INotifyPropertyChanged
962962
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0019");
963963
}
964964

965+
[TestMethod]
966+
public void FieldWithOrphanedDependentObservablePropertyAttributesError_AlsoNotifyChangeFor()
967+
{
968+
string source = @"
969+
using CommunityToolkit.Mvvm.ComponentModel;
970+
971+
namespace MyApp
972+
{
973+
public partial class MyViewModel
974+
{
975+
[AlsoNotifyChangeFor("")]
976+
public int number;
977+
}
978+
}";
979+
980+
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0020");
981+
}
982+
983+
[TestMethod]
984+
public void FieldWithOrphanedDependentObservablePropertyAttributesError_AlsoNotifyCanExecuteFor()
985+
{
986+
string source = @"
987+
using CommunityToolkit.Mvvm.ComponentModel;
988+
989+
namespace MyApp
990+
{
991+
public partial class MyViewModel
992+
{
993+
[AlsoNotifyCanExecuteFor("")]
994+
public int number;
995+
}
996+
}";
997+
998+
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0020");
999+
}
1000+
9651001
/// <summary>
9661002
/// Verifies the output of a source generator.
9671003
/// </summary>

0 commit comments

Comments
 (0)